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 authored and GitHub committed 2024-03-12 14:36:22 +00:00
1 parent 87e63c024e
commit c3c4f49fc2
14 files changed
+296 -12

No files matched your search

+7
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
@@ -20,7 +20,7 @@ import adminSeeder from "../../../../helpers/admin-seeder"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
import { setupTaxStructure } from "../../fixtures" import { setupTaxStructure } from "../../fixtures"
jest.setTimeout(50000) jest.setTimeout(100000)
const env = { MEDUSA_FF_MEDUSA_V2: true } 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 () => { it("should update a cart with promo codes with a replace action", async () => {
await setupTaxStructure(taxModule) await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const targetRules = [ const targetRules = [
{ {
attribute: "product_id", attribute: "product_id",
@@ -365,6 +370,7 @@ medusaIntegrationTestRunner({
const cart = await cartModule.create({ const cart = await cartModule.create({
currency_code: "usd", currency_code: "usd",
email: "tony@stark.com", email: "tony@stark.com",
region_id: region.id,
shipping_address: { shipping_address: {
address_1: "test address 1", address_1: "test address 1",
address_2: "test address 2", address_2: "test address 2",
@@ -393,10 +399,12 @@ medusaIntegrationTestRunner({
}, },
]) ])
await remoteLink.create({ await remoteLink.create([
[Modules.CART]: { cart_id: cart.id }, {
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id }, [Modules.CART]: { cart_id: cart.id },
}) [Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
])
// Should remove earlier adjustments from other promocodes // Should remove earlier adjustments from other promocodes
let updated = await api.post(`/store/carts/${cart.id}`, { 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 () => { it("should update a cart's region, sales channel, customer data and tax lines", async () => {
await setupTaxStructure(taxModule) 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 () => { it("should create and update a cart", async () => {
const region = await regionModule.create({ const region = await regionModule.create({
name: "US", name: "US",
@@ -700,6 +780,11 @@ medusaIntegrationTestRunner({
it("should add item to cart", async () => { it("should add item to cart", async () => {
await setupTaxStructure(taxModule) await setupTaxStructure(taxModule)
const region = await regionModule.create({
name: "US",
currency_code: "usd",
})
const customer = await customerModule.create({ const customer = await customerModule.create({
email: "tony@stark-industries.com", email: "tony@stark-industries.com",
}) })
@@ -723,6 +808,7 @@ medusaIntegrationTestRunner({
const cart = await cartModule.create({ const cart = await cartModule.create({
currency_code: "usd", currency_code: "usd",
customer_id: customer.id, customer_id: customer.id,
region_id: region.id,
shipping_address: { shipping_address: {
customer_id: customer.id, customer_id: customer.id,
address_1: "test address 1", 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",
})
})
})
}) })
}, },
}) })
@@ -9,6 +9,7 @@ import {
TaxableItemDTO, TaxableItemDTO,
TaxableShippingDTO, TaxableShippingDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk" import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { ModuleRegistrationName } from "../../../../../modules-sdk/dist" import { ModuleRegistrationName } from "../../../../../modules-sdk/dist"
@@ -16,14 +17,28 @@ interface StepInput {
cart: CartWorkflowDTO cart: CartWorkflowDTO
items: CartLineItemDTO[] items: CartLineItemDTO[]
shipping_methods: CartShippingMethodDTO[] shipping_methods: CartShippingMethodDTO[]
force_tax_calculation?: boolean
} }
function normalizeTaxModuleContext( function normalizeTaxModuleContext(
cart: CartWorkflowDTO cart: CartWorkflowDTO,
forceTaxCalculation: boolean
): TaxCalculationContext | null { ): TaxCalculationContext | null {
const address = cart.shipping_address 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 return null
} }
@@ -83,12 +98,17 @@ export const getItemTaxLinesStepId = "get-item-tax-lines"
export const getItemTaxLinesStep = createStep( export const getItemTaxLinesStep = createStep(
getItemTaxLinesStepId, getItemTaxLinesStepId,
async (data: StepInput, { container }) => { 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>( const taxService = container.resolve<ITaxModuleService>(
ModuleRegistrationName.TAX ModuleRegistrationName.TAX
) )
const taxContext = normalizeTaxModuleContext(cart) const taxContext = normalizeTaxModuleContext(cart, forceTaxCalculation)
if (!taxContext) { if (!taxContext) {
return new StepResponse({ return new StepResponse({
@@ -10,6 +10,7 @@ interface StepInput {
cart_or_cart_id: CartWorkflowDTO | string cart_or_cart_id: CartWorkflowDTO | string
items?: CartLineItemDTO[] items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[] shipping_methods?: CartShippingMethodDTO[]
force_tax_calculation?: boolean
} }
export const updateTaxLinesStepId = "update-tax-lines-step" export const updateTaxLinesStepId = "update-tax-lines-step"
@@ -18,6 +18,8 @@ const cartFields = [
"id", "id",
"currency_code", "currency_code",
"email", "email",
"region.id",
"region.automatic_taxes",
"items.id", "items.id",
"items.variant_id", "items.variant_id",
"items.product_id", "items.product_id",
@@ -62,6 +64,7 @@ type WorkflowInput = {
cart_or_cart_id: string | CartWorkflowDTO cart_or_cart_id: string | CartWorkflowDTO
items?: CartLineItemDTO[] items?: CartLineItemDTO[]
shipping_methods?: CartShippingMethodDTO[] shipping_methods?: CartShippingMethodDTO[]
force_tax_calculation?: boolean
} }
export const updateTaxLinesWorkflowId = "update-tax-lines" export const updateTaxLinesWorkflowId = "update-tax-lines"
@@ -79,6 +82,7 @@ export const updateTaxLinesWorkflow = createWorkflow(
items: data.input.items || data.cart.items, items: data.input.items || data.cart.items,
shipping_methods: shipping_methods:
data.input.shipping_methods || data.cart.shipping_methods, data.input.shipping_methods || data.cart.shipping_methods,
force_tax_calculation: data.input.force_tax_calculation,
})) }))
) )
@@ -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 })
}
@@ -66,6 +66,7 @@ export const defaultStoreCartFields = [
"region.id", "region.id",
"region.name", "region.name",
"region.currency_code", "region.currency_code",
"region.automatic_taxes",
"sales_channel_id", "sales_channel_id",
// TODO: To be updated when payment sessions are introduces in the Rest API // TODO: To be updated when payment sessions are introduces in the Rest API
@@ -114,3 +114,5 @@ export class StorePostCartsCartReq {
// @Type(() => Discount) // @Type(() => Discount)
// discounts?: Discount[] // discounts?: Discount[]
} }
export class StorePostCartsCartTaxesReq {}
@@ -34,6 +34,7 @@ describe("Region Module Service", () => {
const createdRegion = await service.create({ const createdRegion = await service.create({
name: "Europe", name: "Europe",
currency_code: "EUR", currency_code: "EUR",
automatic_taxes: false,
}) })
expect(createdRegion).toEqual( expect(createdRegion).toEqual(
@@ -42,6 +43,7 @@ describe("Region Module Service", () => {
name: "Europe", name: "Europe",
currency_code: "eur", currency_code: "eur",
countries: [], countries: [],
automatic_taxes: false,
}) })
) )
@@ -75,6 +77,7 @@ describe("Region Module Service", () => {
id: region.id, id: region.id,
name: "North America", name: "North America",
currency_code: "usd", currency_code: "usd",
automatic_taxes: true,
countries: [ countries: [
expect.objectContaining({ expect.objectContaining({
display_name: "Canada", display_name: "Canada",
@@ -148,6 +151,7 @@ describe("Region Module Service", () => {
name: "Americas", name: "Americas",
currency_code: "MXN", currency_code: "MXN",
countries: ["us", "mx"], countries: ["us", "mx"],
automatic_taxes: false,
}) })
const latestRegion = await service.retrieve(createdRegion.id, { const latestRegion = await service.retrieve(createdRegion.id, {
@@ -158,6 +162,7 @@ describe("Region Module Service", () => {
id: createdRegion.id, id: createdRegion.id,
name: "Americas", name: "Americas",
currency_code: "mxn", currency_code: "mxn",
automatic_taxes: false,
}) })
expect(latestRegion.countries.map((c) => c.iso_2)).toEqual(["mx", "us"]) expect(latestRegion.countries.map((c) => c.iso_2)).toEqual(["mx", "us"])
}) })
@@ -20,9 +20,10 @@ ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_3bdd5896ec93be2f1c62a3309a5";
ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_91f88052197680f9790272aaf5b"; ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_91f88052197680f9790272aaf5b";
${generatePostgresAlterColummnIfExistStatement( ${generatePostgresAlterColummnIfExistStatement(
"region", "region",
["tax_rate", "gift_cards_taxable", "automatic_taxes", "includes_tax"], ["tax_rate", "gift_cards_taxable", "includes_tax"],
"DROP NOT NULL" "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 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 or update "region_country" table
CREATE TABLE IF NOT EXISTS "region_country" ( CREATE TABLE IF NOT EXISTS "region_country" (
+3 -1
View File
@@ -6,7 +6,6 @@ import {
Entity, Entity,
Filter, Filter,
Index, Index,
ManyToOne,
OnInit, OnInit,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
@@ -31,6 +30,9 @@ export default class Region {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
currency_code: string currency_code: string
@Property({ columnType: "boolean" })
automatic_taxes = true
@OneToMany(() => Country, (country) => country.region) @OneToMany(() => Country, (country) => country.region)
countries = new Collection<Country>(this) countries = new Collection<Country>(this)
+2
View File
@@ -1,5 +1,6 @@
import { CustomerDTO } from "../customer" import { CustomerDTO } from "../customer"
import { ProductDTO } from "../product" import { ProductDTO } from "../product"
import { RegionDTO } from "../region"
import { CartDTO, CartLineItemDTO } from "./common" import { CartDTO, CartLineItemDTO } from "./common"
import { UpdateLineItemDTO } from "./mutations" import { UpdateLineItemDTO } from "./mutations"
@@ -98,4 +99,5 @@ export interface CreatePaymentCollectionForCartWorkflowInputDTO {
export interface CartWorkflowDTO extends CartDTO { export interface CartWorkflowDTO extends CartDTO {
customer?: CustomerDTO customer?: CustomerDTO
product?: ProductDTO product?: ProductDTO
region?: RegionDTO
} }
+5
View File
@@ -19,6 +19,11 @@ export interface RegionDTO {
*/ */
currency_code: string currency_code: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes: boolean
/** /**
* The countries of the region. * The countries of the region.
*/ */
+12
View File
@@ -10,6 +10,10 @@ export interface CreateRegionDTO {
* The currency code of the region. * The currency code of the region.
*/ */
currency_code: string currency_code: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/** /**
* The region's countries. * The region's countries.
*/ */
@@ -33,6 +37,10 @@ export interface UpsertRegionDTO {
* The currency code of the region. * The currency code of the region.
*/ */
currency_code?: string currency_code?: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/** /**
* The region's countries. * The region's countries.
*/ */
@@ -52,6 +60,10 @@ export interface UpdateRegionDTO {
* The currency code of the region. * The currency code of the region.
*/ */
currency_code?: string currency_code?: string
/**
* Setting to indicate whether taxes need to be applied automatically
*/
automatic_taxes?: boolean
/** /**
* The region's countries. * The region's countries.
*/ */