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:
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { JestUtils } from "medusa-test-utils"
|
|
||||||
|
|
||||||
JestUtils.afterAllHookDropDatabase()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./database"
|
|
||||||
export * from "./get-init-module-config"
|
|
||||||
@@ -17,6 +17,4 @@ module.exports = {
|
|||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
modulePathIgnorePatterns: ["dist/"],
|
modulePathIgnorePatterns: ["dist/"],
|
||||||
setupFiles: ["<rootDir>/integration-tests/setup-env.js"],
|
|
||||||
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
|
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
|
||||||
"build": "rimraf dist && 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": "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: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: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",
|
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import {
|
|||||||
import {
|
import {
|
||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
Cascade,
|
Cascade,
|
||||||
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
OneToMany,
|
||||||
OptionalProps,
|
OptionalProps,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import TaxRegion from "./tax-region"
|
import TaxRegion from "./tax-region"
|
||||||
|
import TaxRateRule from "./tax-rate-rule"
|
||||||
|
|
||||||
type OptionalTaxRateProps = DAL.EntityDateColumns
|
type OptionalTaxRateProps = DAL.EntityDateColumns
|
||||||
|
|
||||||
@@ -63,6 +66,9 @@ export default class TaxRate {
|
|||||||
})
|
})
|
||||||
tax_region: TaxRegion
|
tax_region: TaxRegion
|
||||||
|
|
||||||
|
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate)
|
||||||
|
rules = new Collection<TaxRateRule>(this)
|
||||||
|
|
||||||
@Property({ columnType: "jsonb", nullable: true })
|
@Property({ columnType: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown> | null = null
|
metadata: Record<string, unknown> | null = null
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
|
promiseAll,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { TaxRate, TaxRegion, TaxRateRule } from "@models"
|
import { TaxRate, TaxRegion, TaxRateRule } from "@models"
|
||||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||||
@@ -128,7 +129,7 @@ export default class TaxModuleService<
|
|||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
const rates = regions.map((region, i) => {
|
const rates = regions.map((region: TaxRegionDTO, i: number) => {
|
||||||
return {
|
return {
|
||||||
...defaultRates[i],
|
...defaultRates[i],
|
||||||
tax_region_id: region.id,
|
tax_region_id: region.id,
|
||||||
@@ -166,4 +167,213 @@ export default class TaxModuleService<
|
|||||||
) {
|
) {
|
||||||
return await this.taxRateRuleService_.create(data, sharedContext)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,3 +99,55 @@ export interface FilterableTaxRateRuleProps
|
|||||||
updated_at?: OperatorMap<string>
|
updated_at?: OperatorMap<string>
|
||||||
created_by?: string | string[] | 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
TaxRegionDTO,
|
TaxRegionDTO,
|
||||||
TaxRateRuleDTO,
|
TaxRateRuleDTO,
|
||||||
FilterableTaxRateRuleProps,
|
FilterableTaxRateRuleProps,
|
||||||
|
TaxableItemDTO,
|
||||||
|
TaxCalculationContext,
|
||||||
|
ItemTaxLineDTO,
|
||||||
|
ShippingTaxLineDTO,
|
||||||
|
TaxableShippingDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
CreateTaxRateRuleDTO,
|
CreateTaxRateRuleDTO,
|
||||||
@@ -64,4 +69,10 @@ export interface ITaxModuleService extends IModuleService {
|
|||||||
config?: FindConfig<TaxRateRuleDTO>,
|
config?: FindConfig<TaxRateRuleDTO>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<TaxRateRuleDTO[]>
|
): Promise<TaxRateRuleDTO[]>
|
||||||
|
|
||||||
|
getTaxLines(
|
||||||
|
item: (TaxableItemDTO | TaxableShippingDTO)[],
|
||||||
|
calculationContext: TaxCalculationContext,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<(ItemTaxLineDTO | ShippingTaxLineDTO)[]>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user