feat(tax): adds getItemTaxLines (#6440)

**What**
- Selects the correct tax line for an item given a calculation context.

**For later PR**
- Consider optimizations. Some thoughts:
  - Even with global sales the number of rates in the DB is not likely to grow beyond ~1000.
  - Can large orders with hundreds of items optimize somehow?
  - Does it make sense to write a custom SQL query to do this?
- Support combined rate.

**Test cases covered**
The selection of tax rates take the following priority:

1. specific product rules - province
2. specific product type rules - province
3. default province rules
4. specific product rules - country
5. specific product type rules - country
6. default country rules

There are test cases for each of them under the following data seed structure:

### **US** 
- **Default Rate**: 2%
- **Sub-Regions**
  - CA 
    - Default Rate: 5%
    - Overrides
      - Reduced rate (for 3 product ids): 3%
      - Reduced rate (for product type): 1%
  - NY 
    - Default rate: 6%
  - FL 
    - Default rate: 4%
- **Overrides**
  - None

### **Denmark** 
- **Default rate:** 25% 
- **Sub-Regions**
  - None
- **Overrides**
  -  None

### **Germany** 
- **Default Rate:** 19%
- **Sub-Regions**
  - None
- **Overrides:**
  - Reduced Rate (for product type) - 7%

### **Canada** 
- **Default rate**: 5%
- **Sub-Regions**
  - QC 
    - Default rate: 2%
    - Overrides:
      - Reduced rate (for same product type as country reduced rate): 1%
  - BC 
    - Default rate: 2%
- **Overrides**
  - Reduced rate (for product id) - 3%
  - Reduced rate (for product type) - 3.5%
This commit is contained in:
Sebastian Rindom
2024-02-22 17:28:55 +01:00
committed by GitHub
parent 58943e83fd
commit 598ee6f49c
13 changed files with 824 additions and 201 deletions

View File

@@ -0,0 +1,543 @@
import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
import { ITaxModuleService } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
jest.setTimeout(30000)
moduleIntegrationTestRunner({
moduleName: Modules.TAX,
testSuite: ({ service }: SuiteOptions<ITaxModuleService>) => {
describe("TaxModuleService", function () {
it("should create a tax region", async () => {
const [region] = await service.createTaxRegions([
{
country_code: "US",
default_tax_rate: {
name: "Test Rate",
rate: 0.2,
},
},
])
const [provinceRegion] = await service.createTaxRegions([
{
country_code: "US",
province_code: "CA",
parent_id: region.id,
default_tax_rate: {
name: "CA Rate",
rate: 8.25,
},
},
])
const listedRegions = await service.listTaxRegions()
expect(listedRegions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: region.id,
country_code: "US",
province_code: null,
parent_id: null,
}),
expect.objectContaining({
id: provinceRegion.id,
country_code: "US",
province_code: "CA",
parent_id: region.id,
}),
])
)
const rates = await service.list()
expect(rates).toEqual(
expect.arrayContaining([
expect.objectContaining({
tax_region_id: region.id,
rate: 0.2,
name: "Test Rate",
is_default: true,
}),
expect.objectContaining({
tax_region_id: provinceRegion.id,
rate: 8.25,
name: "CA Rate",
is_default: true,
}),
])
)
})
it("should create a tax rate rule", 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",
rate: 8.23,
})
await service.createTaxRateRules([
{
tax_rate_id: rate.id,
reference: "product",
reference_id: "prod_1234",
},
])
const listedRules = await service.listTaxRateRules(
{},
{
relations: ["tax_rate"],
}
)
expect(listedRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
reference: "product",
reference_id: "prod_1234",
tax_rate: expect.objectContaining({
tax_region_id: region.id,
name: "Shipping Rate",
rate: 8.23,
}),
}),
])
)
})
it("applies specific product rules at the province level", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_1", // Matching the specific product rate for CA province
quantity: 1,
}
const calculationContext = {
address: {
country_code: "US",
province_code: "CA",
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 3, // Expecting the reduced rate for specific products in CA
code: "CAREDUCE_PROD",
name: "CA Reduced Rate for Products",
}),
])
})
it("applies specific product type rules at the province level", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_unknown", // This product does not have a specific rule
product_type_id: "product_type_id_1", // Matching the specific product type rate for CA province
quantity: 1,
}
const calculationContext = {
address: {
country_code: "US",
province_code: "CA",
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 1, // Expecting the reduced rate for specific product types in CA
code: "CAREDUCE_TYPE",
name: "CA Reduced Rate for Product Type",
}),
])
})
it("applies specific product type rules at the province level", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_unknown", // This product does not have a specific rule
product_type_id: "product_type_id_1", // Matching the specific product type rate for CA province
quantity: 1,
}
const calculationContext = {
address: {
country_code: "US",
province_code: "CA",
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 1, // Expecting the reduced rate for specific product types in CA
code: "CAREDUCE_TYPE",
name: "CA Reduced Rate for Product Type",
}),
])
})
it("applies default province rules when no specific product or product type rule matches", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_unknown",
quantity: 1,
}
const calculationContext = {
address: {
country_code: "US",
province_code: "NY", // Testing with NY to apply the default provincial rate
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 6, // Expecting the default rate for NY province
code: "NYDEFAULT",
name: "NY Default Rate",
}),
])
})
it("applies specific product rules at the country level when no province rate applies", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_4", // Assuming this ID now has a specific rule at the country level for Canada
quantity: 1,
}
const calculationContext = {
address: {
country_code: "CA",
province_code: "ON", // This province does not have a specific rule
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 3, // Expecting the reduced rate for specific products in Canada
code: "CAREDUCE_PROD_CA",
name: "Canada Reduced Rate for Product",
}),
])
})
it("applies default country rules when no specific product or product type rule matches", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_unknown",
quantity: 1,
}
const calculationContext = {
address: {
country_code: "DE", // Testing with Germany to apply the default country rate
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 19,
code: "DE19",
name: "Germany Default Rate",
}),
])
})
it("prioritizes specific product rules over product type rules", async () => {
await setupTaxStructure(service)
const item = {
id: "item_test",
product_id: "product_id_1", // This product has a specific rule for product type and product
product_type_id: "product_type_id_1", // This product type has a specific rule for product type
quantity: 1,
}
const calculationContext = {
address: {
country_code: "US",
province_code: "CA",
},
}
const taxLines = await service.getTaxLines([item], calculationContext)
expect(taxLines).toEqual([
expect.objectContaining({
rate_id: expect.any(String),
rate: 3, // Expecting the reduced rate for specific products in CA
code: "CAREDUCE_PROD",
name: "CA Reduced Rate for Products",
}),
])
})
})
},
})
const setupTaxStructure = async (service) => {
// Setup for this specific test
//
// Using the following structure to setup tests.
// US - default 2%
// - Region: CA - default 5%
// - Override: Reduced rate (for 3 product ids): 3%
// - Override: Reduced rate (for product type): 1%
// - Region: NY - default: 6%
// - Region: FL - default: 4%
//
// Denmark - default 25%
//
// Germany - default 19%
// - Override: Reduced Rate (for product type) - 7%
//
// Canada - default 5%
// - Override: Reduced rate (for product id) - 3%
// - Override: Reduced rate (for product type) - 3.5%
// - Region: QC - default 2%
// - Override: Reduced rate (for same product type as country reduced rate): 1%
// - Region: BC - default 2%
//
const [us, dk, de, ca] = await service.createTaxRegions([
{
country_code: "US",
default_tax_rate: { name: "US Default Rate", rate: 2 },
},
{
country_code: "DK",
default_tax_rate: { name: "Denmark Default Rate", rate: 25 },
},
{
country_code: "DE",
default_tax_rate: {
code: "DE19",
name: "Germany Default Rate",
rate: 19,
},
},
{
country_code: "CA",
default_tax_rate: { name: "Canada Default Rate", rate: 5 },
},
])
// Create province regions within the US
const [cal, ny, fl, qc, bc] = await service.createTaxRegions([
{
country_code: "US",
province_code: "CA",
parent_id: us.id,
default_tax_rate: {
rate: 5,
name: "CA Default Rate",
code: "CADEFAULT",
},
},
{
country_code: "US",
province_code: "NY",
parent_id: us.id,
default_tax_rate: {
rate: 6,
name: "NY Default Rate",
code: "NYDEFAULT",
},
},
{
country_code: "US",
province_code: "FL",
parent_id: us.id,
default_tax_rate: {
rate: 4,
name: "FL Default Rate",
code: "FLDEFAULT",
},
},
{
country_code: "CA",
province_code: "QC",
parent_id: ca.id,
default_tax_rate: {
rate: 2,
name: "QC Default Rate",
code: "QCDEFAULT",
},
},
{
country_code: "CA",
province_code: "BC",
parent_id: ca.id,
default_tax_rate: {
rate: 2,
name: "BC Default Rate",
code: "BCDEFAULT",
},
},
])
const [calProd, calType, deType, canProd, canType, qcType] =
await service.create([
{
tax_region_id: cal.id,
name: "CA Reduced Rate for Products",
rate: 3,
code: "CAREDUCE_PROD",
},
{
tax_region_id: cal.id,
name: "CA Reduced Rate for Product Type",
rate: 1,
code: "CAREDUCE_TYPE",
},
{
tax_region_id: de.id,
name: "Germany Reduced Rate for Product Type",
rate: 7,
code: "DEREDUCE_TYPE",
},
{
tax_region_id: ca.id,
name: "Canada Reduced Rate for Product",
rate: 3,
code: "CAREDUCE_PROD_CA",
},
{
tax_region_id: ca.id,
name: "Canada Reduced Rate for Product Type",
rate: 3.5,
code: "CAREDUCE_TYPE_CA",
},
{
tax_region_id: qc.id,
name: "QC Reduced Rate for Product Type",
rate: 1,
code: "QCREDUCE_TYPE",
},
])
// Create tax rate rules for specific products and product types
await service.createTaxRateRules([
{
reference: "product",
reference_id: "product_id_1",
tax_rate_id: calProd.id,
},
{
reference: "product",
reference_id: "product_id_2",
tax_rate_id: calProd.id,
},
{
reference: "product",
reference_id: "product_id_3",
tax_rate_id: calProd.id,
},
{
reference: "product_type",
reference_id: "product_type_id_1",
tax_rate_id: calType.id,
},
{
reference: "product_type",
reference_id: "product_type_id_2",
tax_rate_id: deType.id,
},
{
reference: "product",
reference_id: "product_id_4",
tax_rate_id: canProd.id,
},
{
reference: "product_type",
reference_id: "product_type_id_3",
tax_rate_id: canType.id,
},
{
reference: "product_type",
reference_id: "product_type_id_3",
tax_rate_id: qcType.id,
},
])
return {
us: {
country: us,
children: {
cal: {
province: cal,
overrides: {
calProd,
calType,
},
},
ny: {
province: ny,
overrides: {},
},
fl: {
province: fl,
overrides: {},
},
},
overrides: {},
},
dk: {
country: dk,
children: {},
overrides: {},
},
de: {
country: de,
children: {},
overrides: {
deType,
},
},
ca: {
country: ca,
children: {
qc: {
province: qc,
overrides: {
qcType,
},
},
bc: {
province: bc,
overrides: {},
},
},
overrides: {
canProd,
canType,
},
},
}
}

View File

@@ -1,141 +0,0 @@
import { initModules } from "medusa-test-utils"
import { ITaxModuleService } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
import { MikroOrmWrapper } from "../utils"
import { getInitModuleConfig } from "../utils/get-init-module-config"
jest.setTimeout(30000)
describe("TaxModuleService", function () {
let service: ITaxModuleService
let shutdownFunc: () => Promise<void>
beforeAll(async () => {
const initModulesConfig = getInitModuleConfig()
const { medusaApp, shutdown } = await initModules(initModulesConfig)
service = medusaApp.modules[Modules.TAX]
shutdownFunc = shutdown
})
afterAll(async () => {
await shutdownFunc()
})
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
it("should create a tax region", async () => {
const [region] = await service.createTaxRegions([
{
country_code: "US",
default_tax_rate: {
name: "Test Rate",
rate: 0.2,
},
},
])
const [provinceRegion] = await service.createTaxRegions([
{
country_code: "US",
province_code: "CA",
parent_id: region.id,
default_tax_rate: {
name: "CA Rate",
rate: 8.25,
},
},
])
const listedRegions = await service.listTaxRegions()
expect(listedRegions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: region.id,
country_code: "US",
province_code: null,
parent_id: null,
}),
expect.objectContaining({
id: provinceRegion.id,
country_code: "US",
province_code: "CA",
parent_id: region.id,
}),
])
)
const rates = await service.list()
expect(rates).toEqual(
expect.arrayContaining([
expect.objectContaining({
tax_region_id: region.id,
rate: 0.2,
name: "Test Rate",
is_default: true,
}),
expect.objectContaining({
tax_region_id: provinceRegion.id,
rate: 8.25,
name: "CA Rate",
is_default: true,
}),
])
)
})
it("should create a tax rate rule", 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",
rate: 8.23,
})
await service.createTaxRateRules([
{
tax_rate_id: rate.id,
reference: "product",
reference_id: "prod_1234",
},
])
const listedRules = await service.listTaxRateRules(
{},
{
relations: ["tax_rate"],
}
)
expect(listedRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
reference: "product",
reference_id: "prod_1234",
tax_rate: expect.objectContaining({
tax_region_id: region.id,
name: "Shipping Rate",
rate: 8.23,
}),
}),
])
)
})
})

View File

@@ -1,6 +0,0 @@
if (typeof process.env.DB_TEMP_NAME === "undefined") {
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
process.env.DB_TEMP_NAME = `medusa-tax-integration-${tempName}`
}
process.env.MEDUSA_TAX_DB_SCHEMA = "public"

View File

@@ -1,3 +0,0 @@
import { JestUtils } from "medusa-test-utils"
JestUtils.afterAllHookDropDatabase()

View File

@@ -1,12 +0,0 @@
import { TestDatabaseUtils } from "medusa-test-utils"
import * as Models from "@models"
const mikroOrmEntities = Models as unknown as any[]
export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper({
mikroOrmEntities,
schema: process.env.MEDUSA_ORDER_DB_SCHEMA,
})
export const DB_URL = TestDatabaseUtils.getDatabaseURL()

View File

@@ -1,33 +0,0 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { DB_URL } from "./database"
export function getInitModuleConfig() {
const moduleOptions = {
defaultAdapterOptions: {
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_TAX_DB_SCHEMA,
},
},
}
const injectedDependencies = {}
const modulesConfig_ = {
[Modules.TAX]: {
definition: ModulesDefinition[Modules.TAX],
options: moduleOptions,
},
}
return {
injectedDependencies,
modulesConfig: modulesConfig_,
databaseConfig: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_TAX_DB_SCHEMA,
},
joinerConfig: [],
}
}

View File

@@ -1,2 +0,0 @@
export * from "./database"
export * from "./get-init-module-config"

View File

@@ -17,6 +17,4 @@ module.exports = {
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
setupFiles: ["<rootDir>/integration-tests/setup-env.js"],
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
}

View File

@@ -29,7 +29,7 @@
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
"test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts",
"test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",

View File

@@ -6,14 +6,17 @@ import {
import {
BeforeCreate,
Cascade,
Collection,
Entity,
ManyToOne,
OnInit,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import TaxRegion from "./tax-region"
import TaxRateRule from "./tax-rate-rule"
type OptionalTaxRateProps = DAL.EntityDateColumns
@@ -63,6 +66,9 @@ export default class TaxRate {
})
tax_region: TaxRegion
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate)
rules = new Collection<TaxRateRule>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null

View File

@@ -12,6 +12,7 @@ import {
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
promiseAll,
} from "@medusajs/utils"
import { TaxRate, TaxRegion, TaxRateRule } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -128,7 +129,7 @@ export default class TaxModuleService<
sharedContext
)
const rates = regions.map((region, i) => {
const rates = regions.map((region: TaxRegionDTO, i: number) => {
return {
...defaultRates[i],
tax_region_id: region.id,
@@ -166,4 +167,213 @@ export default class TaxModuleService<
) {
return await this.taxRateRuleService_.create(data, sharedContext)
}
@InjectManager("baseRepository_")
async getTaxLines(
items: (TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO)[],
calculationContext: TaxTypes.TaxCalculationContext,
@MedusaContext() sharedContext: Context = {}
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
const regions = await this.taxRegionService_.list(
{
$or: [
{
country_code: calculationContext.address.country_code,
province_code: null,
},
{
country_code: calculationContext.address.country_code,
province_code: calculationContext.address.province_code,
},
],
},
{},
sharedContext
)
const toReturn = await promiseAll(
items.map(async (item) => {
const regionIds = regions.map((r) => r.id)
const rateQuery = this.getTaxRateQueryForItem(item, regionIds)
const rates = await this.taxRateService_.list(
rateQuery,
{
relations: ["tax_region", "rules"],
},
sharedContext
)
return await this.getTaxRatesForItem(item, rates)
})
)
return toReturn.flat()
}
private async getTaxRatesForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
rates: TTaxRate[]
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
if (!rates.length) {
return []
}
const prioritizedRates = this.prioritizeRates(rates, item)
const rate = prioritizedRates[0]
const ratesToReturn = [this.buildRateForItem(rate, item)]
// If the rate can be combined we need to find the rate's
// parent region and add that rate too. If not we can return now.
if (!(rate.is_combinable && rate.tax_region.parent_id)) {
return ratesToReturn
}
// First parent region rate in prioritized rates
// will be the most granular rate.
const parentRate = prioritizedRates.find(
(r) => r.tax_region.id === rate.tax_region.parent_id
)
if (parentRate) {
ratesToReturn.push(this.buildRateForItem(parentRate, item))
}
return ratesToReturn
}
private buildRateForItem(
rate: TTaxRate,
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
): TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO {
const isShipping = "shipping_option_id" in item
const toReturn = {
rate_id: rate.id,
rate: rate.rate,
code: rate.code,
name: rate.name,
}
if (isShipping) {
return {
...toReturn,
shipping_line_id: item.id,
}
}
return {
...toReturn,
line_item_id: item.id,
}
}
private getTaxRateQueryForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
regionIds: string[]
) {
const isShipping = "shipping_option_id" in item
let ruleQuery = isShipping
? [
{
reference: "shipping_option",
reference_id: item.shipping_option_id,
},
]
: [
{
reference: "product",
reference_id: item.product_id,
},
{
reference: "product_type",
reference_id: item.product_type_id,
},
]
return {
$and: [
{ tax_region_id: regionIds },
{ $or: [{ is_default: true }, { rules: { $or: ruleQuery } }] },
],
}
}
private checkRuleMatches(
rate: TTaxRate,
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
) {
if (rate.rules.length === 0) {
return {
isProductMatch: false,
isProductTypeMatch: false,
isShippingMatch: false,
}
}
let isProductMatch = false
const isShipping = "shipping_option_id" in item
const matchingRules = rate.rules.filter((rule) => {
if (isShipping) {
return (
rule.reference === "shipping" &&
rule.reference_id === item.shipping_option_id
)
}
return (
(rule.reference === "product" &&
rule.reference_id === item.product_id) ||
(rule.reference === "product_type" &&
rule.reference_id === item.product_type_id)
)
})
if (matchingRules.some((rule) => rule.reference === "product")) {
isProductMatch = true
}
return {
isProductMatch,
isProductTypeMatch: matchingRules.length > 0,
isShippingMatch: isShipping && matchingRules.length > 0,
}
}
private prioritizeRates(
rates: TTaxRate[],
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
) {
const decoratedRates: (TTaxRate & {
priority_score: number
})[] = rates.map((rate) => {
const { isProductMatch, isProductTypeMatch, isShippingMatch } =
this.checkRuleMatches(rate, item)
const isProvince = rate.tax_region.province_code !== null
const isDefault = rate.is_default
const decoratedRate = {
...rate,
priority_score: 7,
}
if ((isShippingMatch || isProductMatch) && isProvince) {
decoratedRate.priority_score = 1
} else if (isProductTypeMatch && isProvince) {
decoratedRate.priority_score = 2
} else if (isDefault && isProvince) {
decoratedRate.priority_score = 3
} else if ((isShippingMatch || isProductMatch) && !isProvince) {
decoratedRate.priority_score = 4
} else if (isProductTypeMatch && !isProvince) {
decoratedRate.priority_score = 5
} else if (isDefault && !isProvince) {
decoratedRate.priority_score = 6
}
return decoratedRate
})
return decoratedRates.sort(
(a, b) => (a as any).priority_score - (b as any).priority_score
)
}
}

View File

@@ -99,3 +99,55 @@ export interface FilterableTaxRateRuleProps
updated_at?: OperatorMap<string>
created_by?: string | string[] | OperatorMap<string>
}
export interface TaxableItemDTO {
id: string
product_id: string
product_name?: string
product_category_id?: string
product_categories?: string[]
product_sku?: string
product_type?: string
product_type_id?: string
quantity?: number
unit_price?: number
currency_code?: string
}
export interface TaxableShippingDTO {
id: string
shipping_option_id: string
unit_price?: number
currency_code?: string
}
export interface TaxCalculationContext {
address: {
country_code: string
province_code?: string | null
address_1?: string
address_2?: string | null
city?: string
postal_code?: string
}
customer?: {
id: string
email: string
customer_groups: string[]
}
is_return?: boolean
}
interface TaxLineDTO {
rate_id: string
rate: number | null
code: string | null
name: string
}
export interface ItemTaxLineDTO extends TaxLineDTO {
line_item_id: string
}
export interface ShippingTaxLineDTO extends TaxLineDTO {
shipping_line_id: string
}

View File

@@ -8,6 +8,11 @@ import {
TaxRegionDTO,
TaxRateRuleDTO,
FilterableTaxRateRuleProps,
TaxableItemDTO,
TaxCalculationContext,
ItemTaxLineDTO,
ShippingTaxLineDTO,
TaxableShippingDTO,
} from "./common"
import {
CreateTaxRateRuleDTO,
@@ -64,4 +69,10 @@ export interface ITaxModuleService extends IModuleService {
config?: FindConfig<TaxRateRuleDTO>,
sharedContext?: Context
): Promise<TaxRateRuleDTO[]>
getTaxLines(
item: (TaxableItemDTO | TaxableShippingDTO)[],
calculationContext: TaxCalculationContext,
sharedContext?: Context
): Promise<(ItemTaxLineDTO | ShippingTaxLineDTO)[]>
}