feat(core-flows,medusa,types): add automatic-taxes to region + generate tax lines endpoint (#6667)

what:

- endpoint to generate tax lines
- update workflows to force calculate tax lines with a flag
- added automatic_taxes to region
This commit is contained in:
Riqwan Thamir
2024-03-12 15:36:22 +01:00
committed by GitHub
parent 87e63c024e
commit c3c4f49fc2
14 changed files with 296 additions and 12 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(core-flows,medusa,types): add automatic-taxes to region + generate tax lines endpoint

View File

@@ -20,7 +20,7 @@ import adminSeeder from "../../../../helpers/admin-seeder"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { setupTaxStructure } from "../../fixtures"
jest.setTimeout(50000)
jest.setTimeout(100000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
@@ -327,6 +327,11 @@ medusaIntegrationTestRunner({
it("should update a cart with promo codes with a replace action", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const targetRules = [
{
attribute: "product_id",
@@ -365,6 +370,7 @@ medusaIntegrationTestRunner({
const cart = await cartModule.create({
currency_code: "usd",
email: "tony@stark.com",
region_id: region.id,
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
@@ -393,10 +399,12 @@ medusaIntegrationTestRunner({
},
])
await remoteLink.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
})
await remoteLink.create([
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
])
// Should remove earlier adjustments from other promocodes
let updated = await api.post(`/store/carts/${cart.id}`, {
@@ -455,6 +463,78 @@ medusaIntegrationTestRunner({
)
})
it("should not generate tax lines if region is not present or automatic taxes is false", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
automatic_taxes: false,
})
const cart = await cartModule.create({
currency_code: "usd",
email: "tony@stark.com",
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "NY",
country_code: "US",
province: "NY",
postal_code: "94016",
},
items: [
{
id: "item-1",
unit_price: 2000,
quantity: 1,
title: "Test item",
product_id: "prod_tshirt",
} as any,
],
})
let updated = await api.post(`/store/carts/${cart.id}`, {
email: "another@tax.com",
})
expect(updated.status).toEqual(200)
expect(updated.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
items: [
expect.objectContaining({
id: "item-1",
tax_lines: [],
adjustments: [],
}),
],
})
)
await cartModule.update(cart.id, {
region_id: region.id,
})
updated = await api.post(`/store/carts/${cart.id}`, {
email: "another@tax.com",
})
expect(updated.status).toEqual(200)
expect(updated.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
items: [
expect.objectContaining({
id: "item-1",
adjustments: [],
tax_lines: [],
}),
],
})
)
})
it("should update a cart's region, sales channel, customer data and tax lines", async () => {
await setupTaxStructure(taxModule)
@@ -598,7 +678,7 @@ medusaIntegrationTestRunner({
})
})
describe("GET /store/carts/:id", () => {
describe("POST /store/carts/:id", () => {
it("should create and update a cart", async () => {
const region = await regionModule.create({
name: "US",
@@ -700,6 +780,11 @@ medusaIntegrationTestRunner({
it("should add item to cart", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const customer = await customerModule.create({
email: "tony@stark-industries.com",
})
@@ -723,6 +808,7 @@ medusaIntegrationTestRunner({
const cart = await cartModule.create({
currency_code: "usd",
customer_id: customer.id,
region_id: region.id,
shipping_address: {
customer_id: customer.id,
address_1: "test address 1",
@@ -905,6 +991,99 @@ medusaIntegrationTestRunner({
)
})
})
describe("POST /store/carts/:id/taxes", () => {
it("should update a carts tax lines when region.automatic_taxes is false", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
automatic_taxes: false,
})
const cart = await cartModule.create({
currency_code: "usd",
region_id: region.id,
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "NY",
country_code: "US",
province: "NY",
postal_code: "94016",
},
items: [
{
id: "item-1",
unit_price: 2000,
quantity: 1,
title: "Test item",
product_id: "prod_tshirt",
} as any,
],
})
let updated = await api.post(`/store/carts/${cart.id}/taxes`, {})
expect(updated.status).toEqual(200)
// TODO: validate tax totals when calculations are ready
expect(updated.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
items: [
expect.objectContaining({
id: "item-1",
tax_lines: [
expect.objectContaining({
description: "NY Default Rate",
code: "NYDEFAULT",
rate: 6,
provider_id: "system",
}),
],
adjustments: [],
}),
],
})
)
})
it("should throw error when shipping is not present", async () => {
await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
automatic_taxes: false,
})
const cart = await cartModule.create({
currency_code: "usd",
region_id: region.id,
items: [
{
id: "item-1",
unit_price: 2000,
quantity: 1,
title: "Test item",
product_id: "prod_tshirt",
} as any,
],
})
let error = await api
.post(`/store/carts/${cart.id}/taxes`, {})
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
type: "invalid_data",
message: "country code is required to calculate taxes",
})
})
})
})
},
})

View File

@@ -9,6 +9,7 @@ import {
TaxableItemDTO,
TaxableShippingDTO,
} from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "../../../../../modules-sdk/dist"
@@ -16,14 +17,28 @@ interface StepInput {
cart: CartWorkflowDTO
items: CartLineItemDTO[]
shipping_methods: CartShippingMethodDTO[]
force_tax_calculation?: boolean
}
function normalizeTaxModuleContext(
cart: CartWorkflowDTO
cart: CartWorkflowDTO,
forceTaxCalculation: boolean
): TaxCalculationContext | null {
const address = cart.shipping_address
const shouldCalculateTax = forceTaxCalculation || cart.region?.automatic_taxes
if (!address || !address.country_code) {
if (!shouldCalculateTax) {
return null
}
if (forceTaxCalculation && !address?.country_code) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`country code is required to calculate taxes`
)
}
if (!address?.country_code) {
return null
}
@@ -83,12 +98,17 @@ export const getItemTaxLinesStepId = "get-item-tax-lines"
export const getItemTaxLinesStep = createStep(
getItemTaxLinesStepId,
async (data: StepInput, { container }) => {
const { cart, items, shipping_methods: shippingMethods } = data
const {
cart,
items,
shipping_methods: shippingMethods,
force_tax_calculation: forceTaxCalculation = false,
} = data
const taxService = container.resolve<ITaxModuleService>(
ModuleRegistrationName.TAX
)
const taxContext = normalizeTaxModuleContext(cart)
const taxContext = normalizeTaxModuleContext(cart, forceTaxCalculation)
if (!taxContext) {
return new StepResponse({

View File

@@ -10,6 +10,7 @@ interface StepInput {
cart_or_cart_id: CartWorkflowDTO | string
items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[]
force_tax_calculation?: boolean
}
export const updateTaxLinesStepId = "update-tax-lines-step"

View File

@@ -18,6 +18,8 @@ const cartFields = [
"id",
"currency_code",
"email",
"region.id",
"region.automatic_taxes",
"items.id",
"items.variant_id",
"items.product_id",
@@ -62,6 +64,7 @@ type WorkflowInput = {
cart_or_cart_id: string | CartWorkflowDTO
items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[]
force_tax_calculation?: boolean
}
export const updateTaxLinesWorkflowId = "update-tax-lines"
@@ -79,6 +82,7 @@ export const updateTaxLinesWorkflow = createWorkflow(
items: data.input.items || data.cart.items,
shipping_methods:
data.input.shipping_methods || data.cart.shipping_methods,
force_tax_calculation: data.input.force_tax_calculation,
}))
)

View File

@@ -0,0 +1,43 @@
import { updateTaxLinesWorkflow } from "@medusajs/core-flows"
import { Modules } from "@medusajs/modules-sdk"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { defaultStoreCartFields } from "../../query-config"
import { StorePostCartsCartTaxesReq } from "../../validators"
export const POST = async (
req: MedusaRequest<StorePostCartsCartTaxesReq>,
res: MedusaResponse
) => {
const workflow = updateTaxLinesWorkflow(req.scope)
const { errors } = await workflow.run({
input: {
cart_or_cart_id: req.params.id,
force_tax_calculation: true,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: Modules.CART,
fields: defaultStoreCartFields,
})
const [cart] = await remoteQuery(query, {
cart: { id: req.params.id },
})
// TODO: wrap result with totals when totals calculation is ready
res.status(200).json({ cart })
}

View File

@@ -66,6 +66,7 @@ export const defaultStoreCartFields = [
"region.id",
"region.name",
"region.currency_code",
"region.automatic_taxes",
"sales_channel_id",
// TODO: To be updated when payment sessions are introduces in the Rest API

View File

@@ -114,3 +114,5 @@ export class StorePostCartsCartReq {
// @Type(() => Discount)
// discounts?: Discount[]
}
export class StorePostCartsCartTaxesReq {}

View File

@@ -34,6 +34,7 @@ describe("Region Module Service", () => {
const createdRegion = await service.create({
name: "Europe",
currency_code: "EUR",
automatic_taxes: false,
})
expect(createdRegion).toEqual(
@@ -42,6 +43,7 @@ describe("Region Module Service", () => {
name: "Europe",
currency_code: "eur",
countries: [],
automatic_taxes: false,
})
)
@@ -75,6 +77,7 @@ describe("Region Module Service", () => {
id: region.id,
name: "North America",
currency_code: "usd",
automatic_taxes: true,
countries: [
expect.objectContaining({
display_name: "Canada",
@@ -148,6 +151,7 @@ describe("Region Module Service", () => {
name: "Americas",
currency_code: "MXN",
countries: ["us", "mx"],
automatic_taxes: false,
})
const latestRegion = await service.retrieve(createdRegion.id, {
@@ -158,6 +162,7 @@ describe("Region Module Service", () => {
id: createdRegion.id,
name: "Americas",
currency_code: "mxn",
automatic_taxes: false,
})
expect(latestRegion.countries.map((c) => c.iso_2)).toEqual(["mx", "us"])
})

View File

@@ -20,9 +20,10 @@ ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_3bdd5896ec93be2f1c62a3309a5";
ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_91f88052197680f9790272aaf5b";
${generatePostgresAlterColummnIfExistStatement(
"region",
["tax_rate", "gift_cards_taxable", "automatic_taxes", "includes_tax"],
["tax_rate", "gift_cards_taxable", "includes_tax"],
"DROP NOT NULL"
)}
ALTER TABLE "region" ADD COLUMN IF NOT EXISTS "automatic_taxes" BOOLEAN NOT NULL DEFAULT TRUE;
CREATE INDEX IF NOT EXISTS "IDX_region_deleted_at" ON "region" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
-- Create or update "region_country" table
CREATE TABLE IF NOT EXISTS "region_country" (

View File

@@ -6,7 +6,6 @@ import {
Entity,
Filter,
Index,
ManyToOne,
OnInit,
OneToMany,
OptionalProps,
@@ -31,6 +30,9 @@ export default class Region {
@Property({ columnType: "text" })
currency_code: string
@Property({ columnType: "boolean" })
automatic_taxes = true
@OneToMany(() => Country, (country) => country.region)
countries = new Collection<Country>(this)

View File

@@ -1,5 +1,6 @@
import { CustomerDTO } from "../customer"
import { ProductDTO } from "../product"
import { RegionDTO } from "../region"
import { CartDTO, CartLineItemDTO } from "./common"
import { UpdateLineItemDTO } from "./mutations"
@@ -98,4 +99,5 @@ export interface CreatePaymentCollectionForCartWorkflowInputDTO {
export interface CartWorkflowDTO extends CartDTO {
customer?: CustomerDTO
product?: ProductDTO
region?: RegionDTO
}

View File

@@ -19,6 +19,11 @@ export interface RegionDTO {
*/
currency_code: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes: boolean
/**
* The countries of the region.
*/

View File

@@ -10,6 +10,10 @@ export interface CreateRegionDTO {
* The currency code of the region.
*/
currency_code: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/**
* The region's countries.
*/
@@ -33,6 +37,10 @@ export interface UpsertRegionDTO {
* The currency code of the region.
*/
currency_code?: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/**
* The region's countries.
*/
@@ -52,6 +60,10 @@ export interface UpdateRegionDTO {
* The currency code of the region.
*/
currency_code?: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/**
* The region's countries.
*/