Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tax): add support for updating tax rates #6537

Merged
merged 1 commit into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions integration-tests/plugins/__tests__/tax/admin/tax.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,94 @@ describe("Taxes - Admin", () => {
])
)
})

it("can create a tax rate and update it", 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

expect(regionRes.status).toEqual(200)
expect(regionRes.data).toEqual({
tax_region: {
id: expect.any(String),
country_code: "us",
parent_id: null,
province_code: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
created_by: "admin_user",
provider_id: null,
metadata: null,
},
})

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
)

expect(rateRes.status).toEqual(200)
expect(rateRes.data).toEqual({
tax_rate: {
id: expect.any(String),
code: "RATE2",
rate: 10,
name: "another rate",
is_default: false,
metadata: null,
tax_region_id: usRegionId,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
created_by: "admin_user",
is_combinable: false,
},
})

const updateRes = await api.post(
`/admin/tax-rates/${rateRes.data.tax_rate.id}`,
{
code: "updatedcode",
rate: 12,
is_combinable: true,
name: "Another Name",
metadata: { you: "know it" },
},
adminHeaders
)

expect(updateRes.status).toEqual(200)
expect(updateRes.data).toEqual({
tax_rate: {
id: expect.any(String),
code: "updatedcode",
rate: 12,
name: "Another Name",
is_default: false,
metadata: { you: "know it" },
tax_region_id: usRegionId,
deleted_at: null,
created_at: expect.any(String),
updated_at: expect.any(String),
created_by: "admin_user",
is_combinable: true,
},
})
})
})
1 change: 1 addition & 0 deletions packages/core-flows/src/tax/steps/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./create-tax-regions"
export * from "./create-tax-rates"
export * from "./update-tax-rates"
47 changes: 47 additions & 0 deletions packages/core-flows/src/tax/steps/update-tax-rates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
FilterableTaxRateProps,
ITaxModuleService,
UpdateTaxRateDTO,
} from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"

type UpdateTaxRatesStepInput = {
selector: FilterableTaxRateProps
update: UpdateTaxRateDTO
}

export const updateTaxRatesStepId = "update-tax-rates"
export const updateTaxRatesStep = createStep(
updateTaxRatesStepId,
async (data: UpdateTaxRatesStepInput, { container }) => {
const service = container.resolve<ITaxModuleService>(
ModuleRegistrationName.TAX
)

const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])

const prevData = await service.list(data.selector, {
select: selects,
relations,
})

const taxRates = await service.update(data.selector, data.update)

return new StepResponse(taxRates, prevData)
},
async (prevData, { container }) => {
if (!prevData?.length) {
return
}

const service = container.resolve<ITaxModuleService>(
ModuleRegistrationName.TAX
)

await service.upsert(prevData)
}
)
1 change: 1 addition & 0 deletions packages/core-flows/src/tax/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./create-tax-regions"
export * from "./create-tax-rates"
export * from "./update-tax-rates"
22 changes: 22 additions & 0 deletions packages/core-flows/src/tax/workflows/update-tax-rates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
FilterableTaxRateProps,
TaxRateDTO,
UpdateTaxRateDTO,
} from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateTaxRatesStep } from "../steps"

type UpdateTaxRatesStepInput = {
selector: FilterableTaxRateProps
update: UpdateTaxRateDTO
}

type WorkflowInput = UpdateTaxRatesStepInput

export const updateTaxRatesWorkflowId = "update-tax-rates"
export const updateTaxRatesWorkflow = createWorkflow(
updateTaxRatesWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<TaxRateDTO[]> => {
return updateTaxRatesStep(input)
}
)
31 changes: 31 additions & 0 deletions packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ import {

import { defaultAdminTaxRateFields } from "../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { AdminPostTaxRatesTaxRateReq } from "../../../../api/routes/admin/tax-rates"
import { updateTaxRatesWorkflow } from "@medusajs/core-flows"

export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostTaxRatesTaxRateReq>,
res: MedusaResponse
) => {
const { errors } = await updateTaxRatesWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
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 })
}

export const GET = async (
req: AuthenticatedMedusaRequest,
Expand Down
6 changes: 6 additions & 0 deletions packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as QueryConfig from "./query-config"
import {
AdminGetTaxRatesTaxRateParams,
AdminPostTaxRatesReq,
AdminPostTaxRatesTaxRateReq,
} from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"

Expand All @@ -20,6 +21,11 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/tax-rates",
middlewares: [transformBody(AdminPostTaxRatesReq)],
},
{
method: "POST",
matcher: "/admin/tax-rates/:id",
middlewares: [transformBody(AdminPostTaxRatesTaxRateReq)],
},
{
method: "GET",
matcher: "/admin/tax-rates/:id",
Expand Down
27 changes: 26 additions & 1 deletion packages/medusa/src/api-v2/admin/tax-rates/validators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// HEAD
import { Type } from "class-transformer"
import {
IsBoolean,
Expand Down Expand Up @@ -52,3 +51,29 @@ export class AdminPostTaxRatesReq {
@IsOptional()
metadata?: Record<string, unknown>
}

export class AdminPostTaxRatesTaxRateReq {
@IsOptional()
@IsNumber()
rate?: number | null

@IsOptional()
@IsString()
code?: string | null

@IsString()
@IsOptional()
name?: string

@IsBoolean()
@IsOptional()
is_default?: boolean

@IsBoolean()
@IsOptional()
is_combinable?: boolean

@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
21 changes: 21 additions & 0 deletions packages/tax/src/services/tax-module-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,27 @@ export default class TaxModuleService<
return await this.taxRateService_.update({ selector, data }, sharedContext)
}

async upsert(
data: TaxTypes.UpsertTaxRateDTO[],
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO[]>
async upsert(
data: TaxTypes.UpsertTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO>

@InjectTransactionManager("baseRepository_")
async upsert(
data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const result = await this.taxRateService_.upsert(data, sharedContext)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will vary from module to module, but many times we'd like to do some validation and normalization to the input before doing update/create, so calling the internal create_ and update_ methods might be better. It might be fine here though, just wanted to mention it

const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[]
>(result, { populate: true })
return Array.isArray(data) ? serialized : serialized[0]
}

createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO,
sharedContext?: Context
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/tax/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export interface CreateTaxRateDTO {
metadata?: Record<string, unknown>
}

export interface UpsertTaxRateDTO {
id?: string
rate?: number | null
code?: string | null
name?: string
is_default?: boolean
created_by?: string | null
metadata?: Record<string, unknown> | null
}

export interface UpdateTaxRateDTO {
rate?: number | null
code?: string | null
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/tax/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CreateTaxRateDTO,
CreateTaxRegionDTO,
UpdateTaxRateDTO,
UpsertTaxRateDTO,
} from "./mutations"

export interface ITaxModuleService extends IModuleService {
Expand Down Expand Up @@ -63,6 +64,12 @@ export interface ITaxModuleService extends IModuleService {
sharedContext?: Context
): Promise<TaxRateDTO[]>

upsert(data: UpsertTaxRateDTO, sharedContext?: Context): Promise<TaxRateDTO>
upsert(
data: UpsertTaxRateDTO[],
sharedContext?: Context
): Promise<TaxRateDTO[]>

delete(taxRateIds: string[], sharedContext?: Context): Promise<void>
delete(taxRateId: string, sharedContext?: Context): Promise<void>

Expand Down
Loading