chore(): Reorganize modules (#7210)
**What** Move all modules to the modules directory
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
@@ -0,0 +1,12 @@
|
||||
# @medusajs/tax
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
|
||||
|
||||
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
|
||||
- @medusajs/types@1.11.14
|
||||
- @medusajs/utils@1.11.7
|
||||
- @medusajs/modules-sdk@1.12.9
|
||||
@@ -0,0 +1 @@
|
||||
# Tax Module
|
||||
@@ -0,0 +1,849 @@
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { ITaxModuleService } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { setupTaxStructure } from "../utils/setup-tax-structure"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.TAX,
|
||||
testSuite: ({ service }: SuiteOptions<ITaxModuleService>) => {
|
||||
describe("TaxModuleService", function () {
|
||||
it("should create tax rates and update them", 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",
|
||||
code: "test",
|
||||
rate: 8.23,
|
||||
})
|
||||
|
||||
const updatedRate = await service.update(rate.id, {
|
||||
name: "Updated Rate",
|
||||
code: "TEST",
|
||||
rate: 8.25,
|
||||
})
|
||||
|
||||
expect(updatedRate).toEqual(
|
||||
expect.objectContaining({
|
||||
tax_region_id: region.id,
|
||||
rate: 8.25,
|
||||
name: "Updated Rate",
|
||||
code: "TEST",
|
||||
is_default: false,
|
||||
})
|
||||
)
|
||||
|
||||
const updatedDefaultRate = await service.update(
|
||||
{ tax_region_id: region.id, is_default: true },
|
||||
{ rate: 2 }
|
||||
)
|
||||
|
||||
expect(updatedDefaultRate).toEqual([
|
||||
expect.objectContaining({
|
||||
tax_region_id: region.id,
|
||||
rate: 2,
|
||||
name: "Test Rate",
|
||||
code: null,
|
||||
is_default: true,
|
||||
}),
|
||||
])
|
||||
|
||||
const rates = await service.list()
|
||||
expect(rates).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
tax_region_id: region.id,
|
||||
rate: 2,
|
||||
name: "Test Rate",
|
||||
is_default: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
tax_region_id: region.id,
|
||||
rate: 8.25,
|
||||
name: "Updated Rate",
|
||||
code: "TEST",
|
||||
is_default: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should update tax rates with rules", 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",
|
||||
code: "test",
|
||||
rate: 8.23,
|
||||
})
|
||||
|
||||
await service.update(rate.id, {
|
||||
name: "Updated Rate",
|
||||
code: "TEST",
|
||||
rate: 8.25,
|
||||
rules: [
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
{ reference: "product_type", reference_id: "product_type_id" },
|
||||
],
|
||||
})
|
||||
|
||||
const rules = await service.listTaxRateRules({ tax_rate_id: rate.id })
|
||||
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "product_id_1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
reference: "product_type",
|
||||
reference_id: "product_type_id",
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
await service.update(rate.id, {
|
||||
rules: [
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
{ reference: "product", reference_id: "product_id_2" },
|
||||
{ reference: "product_type", reference_id: "product_type_id_2" },
|
||||
{ reference: "product_type", reference_id: "product_type_id_3" },
|
||||
],
|
||||
})
|
||||
|
||||
const rulesWithDeletes = await service.listTaxRateRules(
|
||||
{ tax_rate_id: rate.id },
|
||||
{ withDeleted: true }
|
||||
)
|
||||
|
||||
expect(rulesWithDeletes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "product_id_2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
reference: "product_type",
|
||||
reference_id: "product_type_id_2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
reference: "product_type",
|
||||
reference_id: "product_type_id_3",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "product_id_1",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
reference: "product_type",
|
||||
reference_id: "product_type_id",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
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",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should delete tax rate", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
const taxRate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
})
|
||||
|
||||
await service.delete(taxRate.id)
|
||||
|
||||
const rates = await service.list({ tax_region_id: region.id })
|
||||
|
||||
expect(rates).toEqual([])
|
||||
})
|
||||
|
||||
it("should soft delete tax rate", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
const taxRate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
})
|
||||
|
||||
await service.softDelete([taxRate.id])
|
||||
|
||||
const rates = await service.list(
|
||||
{ tax_region_id: region.id },
|
||||
{ withDeleted: true }
|
||||
)
|
||||
|
||||
expect(rates).toEqual([
|
||||
expect.objectContaining({
|
||||
id: taxRate.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should delete a tax region and its rates", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
default_tax_rate: {
|
||||
value: 2,
|
||||
code: "test",
|
||||
name: "default test",
|
||||
},
|
||||
})
|
||||
|
||||
await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
})
|
||||
|
||||
await service.deleteTaxRegions(region.id)
|
||||
|
||||
const taxRegions = await service.listTaxRegions()
|
||||
const rates = await service.list()
|
||||
|
||||
expect(taxRegions).toEqual([])
|
||||
expect(rates).toEqual([])
|
||||
})
|
||||
|
||||
it("should soft delete a tax region and its rates", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
default_tax_rate: {
|
||||
value: 2,
|
||||
code: "test",
|
||||
name: "default test",
|
||||
},
|
||||
})
|
||||
|
||||
await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
})
|
||||
|
||||
await service.softDeleteTaxRegions([region.id])
|
||||
|
||||
const taxRegions = await service.listTaxRegions(
|
||||
{},
|
||||
{ withDeleted: true }
|
||||
)
|
||||
const rates = await service.list({}, { withDeleted: true })
|
||||
|
||||
expect(taxRegions).toEqual([
|
||||
expect.objectContaining({
|
||||
id: region.id,
|
||||
country_code: "us",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
expect(rates).toEqual([
|
||||
expect.objectContaining({
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should delete a tax rate and its rules", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
const rate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
rules: [
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
{ reference: "product_type", reference_id: "product_type_id" },
|
||||
],
|
||||
})
|
||||
|
||||
await service.delete(rate.id)
|
||||
|
||||
const taxRegions = await service.listTaxRegions()
|
||||
const rates = await service.list()
|
||||
const rules = await service.listTaxRateRules()
|
||||
|
||||
expect(taxRegions).toEqual([expect.objectContaining({ id: region.id })])
|
||||
expect(rates).toEqual([])
|
||||
expect(rules).toEqual([])
|
||||
})
|
||||
|
||||
it("should soft delete a tax rate and its rules", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
const rate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
rules: [
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
{ reference: "product_type", reference_id: "product_type_id" },
|
||||
],
|
||||
})
|
||||
|
||||
await service.softDelete(rate.id)
|
||||
|
||||
const taxRegions = await service.listTaxRegions(
|
||||
{},
|
||||
{ withDeleted: true }
|
||||
)
|
||||
const rates = await service.list({}, { withDeleted: true })
|
||||
const rules = await service.listTaxRateRules({}, { withDeleted: true })
|
||||
|
||||
expect(taxRegions).toEqual([
|
||||
expect.objectContaining({ id: region.id, deleted_at: null }),
|
||||
])
|
||||
expect(rates).toEqual([
|
||||
expect.objectContaining({
|
||||
id: rate.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
expect(rules).toEqual([
|
||||
expect.objectContaining({
|
||||
tax_rate_id: rate.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
tax_rate_id: rate.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should soft delete a tax rule", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
const rate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
})
|
||||
|
||||
const [ruleOne, ruleTwo] = await service.createTaxRateRules([
|
||||
{
|
||||
tax_rate_id: rate.id,
|
||||
reference: "product",
|
||||
reference_id: "product_id_1",
|
||||
},
|
||||
{
|
||||
tax_rate_id: rate.id,
|
||||
reference: "product_type",
|
||||
reference_id: "product_type_id",
|
||||
},
|
||||
])
|
||||
|
||||
await service.softDeleteTaxRateRules([ruleOne.id])
|
||||
|
||||
const rules = await service.listTaxRateRules({}, { withDeleted: true })
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: ruleOne.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: ruleTwo.id,
|
||||
deleted_at: null,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const rateWithRules = await service.retrieve(rate.id, {
|
||||
relations: ["rules"],
|
||||
})
|
||||
expect(rateWithRules.rules.length).toBe(1)
|
||||
|
||||
// should be possible to add the rule back again
|
||||
await service.createTaxRateRules({
|
||||
tax_rate_id: rate.id,
|
||||
reference: ruleOne.reference,
|
||||
reference_id: ruleOne.reference_id,
|
||||
})
|
||||
|
||||
const rateWithRulesAfterReAdd = await service.retrieve(rate.id, {
|
||||
relations: ["rules"],
|
||||
})
|
||||
expect(rateWithRulesAfterReAdd.rules.length).toBe(2)
|
||||
})
|
||||
|
||||
it("should fail on duplicate rules", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "US",
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
rules: [
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
{ reference: "product", reference_id: "product_id_1" },
|
||||
],
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
/Tax rate rule with tax_rate_id: .*?, reference_id: product_id_1 already exists./
|
||||
)
|
||||
|
||||
const rate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
value: 10,
|
||||
code: "test",
|
||||
name: "test",
|
||||
rules: [{ reference: "product", reference_id: "product_id_1" }],
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.createTaxRateRules({
|
||||
tax_rate_id: rate.id,
|
||||
reference: "product",
|
||||
reference_id: "product_id_1",
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
/Tax rate rule with tax_rate_id: .*?, reference_id: product_id_1 already exists./
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to create province region belonging to a parent with non-matching country", async () => {
|
||||
const caRegion = await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
})
|
||||
await expect(
|
||||
service.createTaxRegions({
|
||||
country_code: "US", // This should be CA
|
||||
parent_id: caRegion.id,
|
||||
province_code: "QC",
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`Province region must belong to a parent region with the same country code. You are trying to create a province region with (country: us, province: qc) but parent expects (country: ca)`
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail duplicate regions", async () => {
|
||||
await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
})
|
||||
|
||||
await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
province_code: "QC",
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
province_code: "QC",
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
"Tax region with country_code: ca, province_code: qc already exists."
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to create region with non-existing parent", async () => {
|
||||
await expect(
|
||||
service.createTaxRegions({
|
||||
parent_id: "something random",
|
||||
country_code: "CA",
|
||||
province_code: "QC",
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`Province region must belong to a parent region. You are trying to create a province region with (country: ca, province: qc) but parent does not exist`
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to create two default tax rates", async () => {
|
||||
const rate = await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
default_tax_rate: {
|
||||
name: "Test Rate",
|
||||
rate: 0.2,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.create({
|
||||
tax_region_id: rate.id,
|
||||
name: "Shipping Rate",
|
||||
rate: 8.23,
|
||||
is_default: true,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
/Tax rate with tax_region_id: .*? already exists./
|
||||
)
|
||||
})
|
||||
|
||||
it("should delete all child regions when parent region is deleted", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
})
|
||||
const provinceRegion = await service.createTaxRegions({
|
||||
parent_id: region.id,
|
||||
country_code: "CA",
|
||||
province_code: "QC",
|
||||
})
|
||||
|
||||
await service.deleteTaxRegions(region.id)
|
||||
|
||||
const taxRegions = await service.listTaxRegions({
|
||||
id: provinceRegion.id,
|
||||
})
|
||||
|
||||
expect(taxRegions).toEqual([])
|
||||
})
|
||||
|
||||
it("should soft delete all child regions when parent region is deleted", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "CA",
|
||||
})
|
||||
const provinceRegion = await service.createTaxRegions({
|
||||
parent_id: region.id,
|
||||
country_code: "CA",
|
||||
province_code: "QC",
|
||||
})
|
||||
|
||||
await service.softDeleteTaxRegions([region.id])
|
||||
|
||||
const taxRegions = await service.listTaxRegions(
|
||||
{ id: provinceRegion.id },
|
||||
{ withDeleted: true }
|
||||
)
|
||||
|
||||
expect(taxRegions).toEqual([
|
||||
expect.objectContaining({
|
||||
id: provinceRegion.id,
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,242 @@
|
||||
import { ITaxModuleService } from "@medusajs/types"
|
||||
|
||||
/**
|
||||
* 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%
|
||||
*/
|
||||
export const setupTaxStructure = async (service: ITaxModuleService) => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
"^@models": "<rootDir>/src/models",
|
||||
"^@services": "<rootDir>/src/services",
|
||||
"^@repositories": "<rootDir>/src/repositories",
|
||||
"^@types": "<rootDir>/src/types",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: "tsconfig.spec.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
modulePathIgnorePatterns: ["dist/"],
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TSMigrationGenerator } from "@medusajs/utils"
|
||||
import * as entities from "./src/models"
|
||||
|
||||
module.exports = {
|
||||
entities: Object.values(entities),
|
||||
schema: "public",
|
||||
clientUrl: "postgres://postgres@localhost/medusa-tax",
|
||||
type: "postgresql",
|
||||
migrations: {
|
||||
generator: TSMigrationGenerator,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@medusajs/tax",
|
||||
"version": "0.1.1",
|
||||
"description": "Medusa Tax module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"bin": {
|
||||
"medusa-tax-seed": "dist/scripts/bin/run-seed.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/tax"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
"watch:test": "tsc --build tsconfig.spec.json --watch",
|
||||
"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 --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",
|
||||
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
|
||||
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "5.9.7",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^29.6.3",
|
||||
"medusa-test-utils": "^1.1.42",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.12.9",
|
||||
"@medusajs/types": "^1.11.14",
|
||||
"@medusajs/utils": "^1.11.7",
|
||||
"@mikro-orm/core": "5.9.7",
|
||||
"@mikro-orm/migrations": "5.9.7",
|
||||
"@mikro-orm/postgresql": "5.9.7",
|
||||
"awilix": "^8.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"knex": "2.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { initializeFactory, Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleDefinition } from "./module-definition"
|
||||
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
|
||||
export const initialize = initializeFactory({
|
||||
moduleName: Modules.TAX,
|
||||
moduleDefinition,
|
||||
})
|
||||
export const runMigrations = moduleDefinition.runMigrations
|
||||
export const revertMigration = moduleDefinition.revertMigration
|
||||
export default moduleDefinition
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
|
||||
|
||||
export const LinkableKeys: Record<string, string> = {
|
||||
tax_rate_id: TaxRate.name,
|
||||
tax_region_id: TaxRegion.name,
|
||||
tax_rate_rule_id: TaxRateRule.name,
|
||||
tax_provider_id: TaxProvider.name,
|
||||
}
|
||||
|
||||
const entityLinkableKeysMap: MapToConfig = {}
|
||||
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
||||
entityLinkableKeysMap[value] ??= []
|
||||
entityLinkableKeysMap[value].push({
|
||||
mapTo: key,
|
||||
valueFrom: key.split("_").pop()!,
|
||||
})
|
||||
})
|
||||
|
||||
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
|
||||
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.TAX,
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: LinkableKeys,
|
||||
alias: [
|
||||
{
|
||||
name: ["tax_rate", "tax_rates"],
|
||||
args: {
|
||||
entity: TaxRate.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["tax_region", "tax_regions"],
|
||||
args: {
|
||||
entity: TaxRegion.name,
|
||||
methodSuffix: "TaxRegions",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["tax_rate_rule", "tax_rate_rules"],
|
||||
args: {
|
||||
entity: TaxRateRule.name,
|
||||
methodSuffix: "TaxRateRules",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["tax_provider", "tax_providers"],
|
||||
args: {
|
||||
entity: TaxProvider.name,
|
||||
methodSuffix: "TaxProviders",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as ModuleJoinerConfig
|
||||
@@ -0,0 +1,43 @@
|
||||
import { moduleProviderLoader } from "@medusajs/modules-sdk"
|
||||
|
||||
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { Lifetime, asFunction } from "awilix"
|
||||
|
||||
import * as providers from "../providers"
|
||||
|
||||
const registrationFn = async (klass, container, pluginOptions) => {
|
||||
container.register({
|
||||
[`tp_${klass.identifier}`]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginOptions),
|
||||
{ lifetime: klass.LIFE_TIME || Lifetime.SINGLETON }
|
||||
),
|
||||
})
|
||||
|
||||
container.registerAdd(
|
||||
"tax_providers",
|
||||
asFunction((cradle) => new klass(cradle, pluginOptions), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export default async ({
|
||||
container,
|
||||
options,
|
||||
}: LoaderOptions<
|
||||
(
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
) & { providers: ModuleProvider[] }
|
||||
>): Promise<void> => {
|
||||
// Local providers
|
||||
for (const provider of Object.values(providers)) {
|
||||
await registrationFn(provider, container, {})
|
||||
}
|
||||
|
||||
await moduleProviderLoader({
|
||||
container,
|
||||
providers: options?.providers || [],
|
||||
registerServiceFn: registrationFn,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"is_enabled": {
|
||||
"name": "is_enabled",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "true",
|
||||
"mappedType": "boolean"
|
||||
}
|
||||
},
|
||||
"name": "tax_provider",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "tax_provider_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"country_code": {
|
||||
"name": "country_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"province_code": {
|
||||
"name": "province_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "tax_region",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"parent_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_tax_region_parent_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_region_deleted_at",
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_region_deleted_at\" ON \"tax_region\" (deleted_at) WHERE deleted_at IS NOT NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_region_unique_country_province",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tax_region_unique_country_province\" ON \"tax_region\" (country_code, province_code)"
|
||||
},
|
||||
{
|
||||
"keyName": "tax_region_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"name": "CK_tax_region_country_top_level",
|
||||
"expression": "parent_id IS NULL OR province_code IS NOT NULL",
|
||||
"definition": "check ((parent_id IS NULL OR province_code IS NOT NULL))"
|
||||
},
|
||||
{
|
||||
"name": "CK_tax_region_provider_top_level",
|
||||
"expression": "parent_id IS NULL OR provider_id IS NULL",
|
||||
"definition": "check ((parent_id IS NULL OR provider_id IS NULL))"
|
||||
}
|
||||
],
|
||||
"foreignKeys": {
|
||||
"tax_region_provider_id_foreign": {
|
||||
"constraintName": "tax_region_provider_id_foreign",
|
||||
"columnNames": [
|
||||
"provider_id"
|
||||
],
|
||||
"localTableName": "public.tax_region",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.tax_provider",
|
||||
"deleteRule": "set null",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"tax_region_parent_id_foreign": {
|
||||
"constraintName": "tax_region_parent_id_foreign",
|
||||
"columnNames": [
|
||||
"parent_id"
|
||||
],
|
||||
"localTableName": "public.tax_region",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.tax_region",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rate": {
|
||||
"name": "rate",
|
||||
"type": "real",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "float"
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "bool",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "false",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"is_combinable": {
|
||||
"name": "is_combinable",
|
||||
"type": "bool",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "false",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"tax_region_id": {
|
||||
"name": "tax_region_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "tax_rate",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_tax_rate_tax_region_id",
|
||||
"columnNames": [
|
||||
"tax_region_id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_tax_region_id\" ON \"tax_rate\" (tax_region_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_rate_deleted_at",
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_deleted_at\" ON \"tax_rate\" (deleted_at) WHERE deleted_at IS NOT NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_single_default_region",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_single_default_region\" ON \"tax_rate\" (tax_region_id) WHERE is_default = true AND deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "tax_rate_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"tax_rate_tax_region_id_foreign": {
|
||||
"constraintName": "tax_rate_tax_region_id_foreign",
|
||||
"columnNames": [
|
||||
"tax_region_id"
|
||||
],
|
||||
"localTableName": "public.tax_rate",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.tax_region",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"tax_rate_id": {
|
||||
"name": "tax_rate_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reference_id": {
|
||||
"name": "reference_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reference": {
|
||||
"name": "reference",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "tax_rate_rule",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_tax_rate_rule_tax_rate_id",
|
||||
"columnNames": [
|
||||
"tax_rate_id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_tax_rate_id\" ON \"tax_rate_rule\" (tax_rate_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_rate_rule_reference_id",
|
||||
"columnNames": [
|
||||
"reference_id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_reference_id\" ON \"tax_rate_rule\" (reference_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_rate_rule_deleted_at",
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_deleted_at\" ON \"tax_rate_rule\" (deleted_at) WHERE deleted_at IS NOT NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_tax_rate_rule_unique_rate_reference",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_unique_rate_reference\" ON \"tax_rate_rule\" (tax_rate_id, reference_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "tax_rate_rule_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"tax_rate_rule_tax_rate_id_foreign": {
|
||||
"constraintName": "tax_rate_rule_tax_rate_id_foreign",
|
||||
"columnNames": [
|
||||
"tax_rate_id"
|
||||
],
|
||||
"localTableName": "public.tax_rate_rule",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.tax_rate",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { generatePostgresAlterColummnIfExistStatement } from "@medusajs/utils"
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240227090331 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
// Adjust tax_provider table
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_provider" ADD COLUMN IF NOT EXISTS "is_enabled" bool not null default true;`
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
`CREATE TABLE IF NOT EXISTS "tax_provider" ("id" text not null, "is_enabled" boolean not null default true, CONSTRAINT "tax_provider_pkey" PRIMARY KEY ("id"));`
|
||||
)
|
||||
|
||||
// Create or update tax_region table
|
||||
this.addSql(
|
||||
`CREATE TABLE IF NOT EXISTS "tax_region" ("id" text not null, "provider_id" text null, "country_code" text not null, "province_code" text null, "parent_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, CONSTRAINT "tax_region_pkey" PRIMARY KEY ("id"), CONSTRAINT "CK_tax_region_country_top_level" CHECK (parent_id IS NULL OR province_code IS NOT NULL), CONSTRAINT "CK_tax_region_provider_top_level" CHECK (parent_id IS NULL OR provider_id IS NULL));`
|
||||
)
|
||||
// Indexes for tax_region
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_region_parent_id" ON "tax_region" ("parent_id");`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_region_deleted_at" ON "tax_region" ("deleted_at") WHERE deleted_at IS NOT NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tax_region_unique_country_province" ON "tax_region" ("country_code", "province_code");`
|
||||
)
|
||||
|
||||
// Old foreign key on region_id
|
||||
this.addSql(
|
||||
generatePostgresAlterColummnIfExistStatement(
|
||||
"tax_rate",
|
||||
["region_id"],
|
||||
"DROP NOT NULL"
|
||||
)
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" DROP CONSTRAINT IF EXISTS "FK_b95a1e03b051993d208366cb960";`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "tax_region_id" text not null;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "deleted_at" timestamptz null;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "created_by" text null;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "is_default" bool not null default false;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "is_combinable" bool not null default false;`
|
||||
)
|
||||
this.addSql(
|
||||
`create table if not exists "tax_rate" ("id" text not null, "rate" real null, "code" text null, "name" text not null, "is_default" bool not null default false, "is_combinable" bool not null default false, "tax_region_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, constraint "tax_rate_pkey" primary key ("id"));`
|
||||
)
|
||||
|
||||
// Indexes for tax_rate
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_tax_region_id" ON "tax_rate" ("tax_region_id") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_deleted_at" ON "tax_rate" ("deleted_at") WHERE deleted_at IS NOT NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_single_default_region" ON "tax_rate" ("tax_region_id") WHERE is_default = true AND deleted_at IS NULL;`
|
||||
)
|
||||
|
||||
// Adjust or create tax_rate_rule table
|
||||
this.addSql(
|
||||
`CREATE TABLE IF NOT EXISTS "tax_rate_rule" ("id" text not null, "tax_rate_id" text not null, "reference_id" text not null, "reference" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, CONSTRAINT "tax_rate_rule_pkey" PRIMARY KEY ("id"));`
|
||||
)
|
||||
// Indexes for tax_rate_rule
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_tax_rate_id" ON "tax_rate_rule" ("tax_rate_id") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_reference_id" ON "tax_rate_rule" ("reference_id") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_deleted_at" ON "tax_rate_rule" ("deleted_at") WHERE deleted_at IS NOT NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tax_rate_rule_unique_rate_reference" ON "tax_rate_rule" ("tax_rate_id", "reference_id") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
|
||||
// Foreign keys adjustments
|
||||
this.addSql(
|
||||
`ALTER TABLE "tax_region" ADD CONSTRAINT "FK_tax_region_provider_id" FOREIGN KEY ("provider_id") REFERENCES "tax_provider" ("id") ON DELETE SET NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE "tax_region" ADD CONSTRAINT "FK_tax_region_parent_id" FOREIGN KEY ("parent_id") REFERENCES "tax_region" ("id") ON DELETE CASCADE;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE "tax_rate" ADD CONSTRAINT "FK_tax_rate_tax_region_id" FOREIGN KEY ("tax_region_id") REFERENCES "tax_region" ("id") ON DELETE CASCADE;`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE "tax_rate_rule" ADD CONSTRAINT "FK_tax_rate_rule_tax_rate_id" FOREIGN KEY ("tax_rate_id") REFERENCES "tax_rate" ("id") ON DELETE CASCADE;`
|
||||
)
|
||||
|
||||
// remove old tax related foreign key constraints
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "product_tax_rate" DROP CONSTRAINT IF EXISTS "FK_346e0016cf045b9980747747645";`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "product_type_tax_rate" DROP CONSTRAINT IF EXISTS "FK_ece65a774192b34253abc4cd672";`
|
||||
)
|
||||
this.addSql(
|
||||
`ALTER TABLE IF EXISTS "shipping_tax_rate" DROP CONSTRAINT IF EXISTS "FK_346e0016cf045b9980747747645";`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as TaxRate } from "./tax-rate"
|
||||
export { default as TaxRegion } from "./tax-region"
|
||||
export { default as TaxRateRule } from "./tax-rate-rule"
|
||||
export { default as TaxProvider } from "./tax-provider"
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
|
||||
const TABLE_NAME = "tax_provider"
|
||||
@Entity({ tableName: TABLE_NAME })
|
||||
export default class TaxProvider {
|
||||
[OptionalProps]?: "is_enabled"
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id: string
|
||||
|
||||
@Property({
|
||||
default: true,
|
||||
columnType: "boolean",
|
||||
})
|
||||
is_enabled: boolean = true
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
Cascade,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Filter,
|
||||
OptionalProps,
|
||||
BeforeCreate,
|
||||
OnInit,
|
||||
} from "@mikro-orm/core"
|
||||
import TaxRate from "./tax-rate"
|
||||
|
||||
const TABLE_NAME = "tax_rate_rule"
|
||||
|
||||
type OptionalRuleProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
const taxRateIdIndexName = "IDX_tax_rate_rule_tax_rate_id"
|
||||
const taxRateIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: taxRateIdIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: "tax_rate_id",
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const referenceIdIndexName = "IDX_tax_rate_rule_reference_id"
|
||||
const referenceIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: referenceIdIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: "reference_id",
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
export const uniqueRateReferenceIndexName =
|
||||
"IDX_tax_rate_rule_unique_rate_reference"
|
||||
const uniqueRateReferenceIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: uniqueRateReferenceIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: ["tax_rate_id", "reference_id"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
@Entity({ tableName: TABLE_NAME })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
@uniqueRateReferenceIndexStatement.MikroORMIndex()
|
||||
export default class TaxRateRule {
|
||||
[OptionalProps]?: OptionalRuleProps
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => TaxRate, {
|
||||
type: "text",
|
||||
fieldName: "tax_rate_id",
|
||||
mapToPk: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
@taxRateIdIndexStatement.MikroORMIndex()
|
||||
tax_rate_id: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@referenceIdIndexStatement.MikroORMIndex()
|
||||
reference_id: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
reference: string
|
||||
|
||||
@ManyToOne(() => TaxRate, { persist: false })
|
||||
tax_rate: TaxRate
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null = null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
created_by: string | null = null
|
||||
|
||||
@createPsqlIndexStatementHelper({
|
||||
tableName: TABLE_NAME,
|
||||
columns: "deleted_at",
|
||||
where: "deleted_at IS NOT NULL",
|
||||
}).MikroORMIndex()
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date | null = null
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "txrule")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "txrule")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import TaxRateRule from "./tax-rate-rule"
|
||||
import TaxRegion from "./tax-region"
|
||||
|
||||
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
const TABLE_NAME = "tax_rate"
|
||||
|
||||
export const singleDefaultRegionIndexName = "IDX_single_default_region"
|
||||
const singleDefaultRegionIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: singleDefaultRegionIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: "tax_region_id",
|
||||
unique: true,
|
||||
where: "is_default = true AND deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const taxRegionIdIndexName = "IDX_tax_rate_tax_region_id"
|
||||
const taxRegionIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: taxRegionIdIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: "tax_region_id",
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
@singleDefaultRegionIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: TABLE_NAME })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
export default class TaxRate {
|
||||
[OptionalProps]?: OptionalTaxRateProps
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "real", nullable: true })
|
||||
rate: number | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
code: string | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@Property({ columnType: "bool", default: false })
|
||||
is_default = false
|
||||
|
||||
@Property({ columnType: "bool", default: false })
|
||||
is_combinable = false
|
||||
|
||||
@ManyToOne(() => TaxRegion, {
|
||||
columnType: "text",
|
||||
fieldName: "tax_region_id",
|
||||
mapToPk: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
@taxRegionIdIndexStatement.MikroORMIndex()
|
||||
tax_region_id: string
|
||||
|
||||
@ManyToOne({ entity: () => TaxRegion, persist: false })
|
||||
tax_region: TaxRegion
|
||||
|
||||
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
|
||||
cascade: ["soft-remove" as Cascade],
|
||||
})
|
||||
rules = new Collection<TaxRateRule>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null = null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
created_by: string | null = null
|
||||
|
||||
@createPsqlIndexStatementHelper({
|
||||
tableName: TABLE_NAME,
|
||||
columns: "deleted_at",
|
||||
where: "deleted_at IS NOT NULL",
|
||||
}).MikroORMIndex()
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date | null = null
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "txr")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "txr")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Check,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import TaxProvider from "./tax-provider"
|
||||
import TaxRate from "./tax-rate"
|
||||
|
||||
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
const TABLE_NAME = "tax_region"
|
||||
|
||||
export const countryCodeProvinceIndexName =
|
||||
"IDX_tax_region_unique_country_province"
|
||||
const countryCodeProvinceIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: countryCodeProvinceIndexName,
|
||||
tableName: TABLE_NAME,
|
||||
columns: ["country_code", "province_code"],
|
||||
unique: true,
|
||||
})
|
||||
|
||||
export const taxRegionProviderTopLevelCheckName =
|
||||
"CK_tax_region_provider_top_level"
|
||||
export const taxRegionCountryTopLevelCheckName =
|
||||
"CK_tax_region_country_top_level"
|
||||
|
||||
@Check({
|
||||
name: taxRegionProviderTopLevelCheckName,
|
||||
expression: `parent_id IS NULL OR provider_id IS NULL`,
|
||||
})
|
||||
@Check({
|
||||
name: taxRegionCountryTopLevelCheckName,
|
||||
expression: `parent_id IS NULL OR province_code IS NOT NULL`,
|
||||
})
|
||||
@countryCodeProvinceIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: TABLE_NAME })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
export default class TaxRegion {
|
||||
[OptionalProps]?: OptionalTaxRegionProps
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => TaxProvider, {
|
||||
fieldName: "provider_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
})
|
||||
provider_id: string | null = null
|
||||
|
||||
@ManyToOne(() => TaxProvider, { persist: false })
|
||||
provider: TaxProvider
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
country_code: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
province_code: string | null = null
|
||||
|
||||
@ManyToOne(() => TaxRegion, {
|
||||
index: "IDX_tax_region_parent_id",
|
||||
fieldName: "parent_id",
|
||||
onDelete: "cascade",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
})
|
||||
parent_id: string | null = null
|
||||
|
||||
@ManyToOne(() => TaxRegion, { persist: false })
|
||||
parent: TaxRegion
|
||||
|
||||
@OneToMany(() => TaxRate, (label) => label.tax_region, {
|
||||
cascade: ["soft-remove" as Cascade],
|
||||
})
|
||||
tax_rates = new Collection<TaxRate>(this)
|
||||
|
||||
@OneToMany(() => TaxRegion, (label) => label.parent, {
|
||||
cascade: ["soft-remove" as Cascade],
|
||||
})
|
||||
children = new Collection<TaxRegion>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null = null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
created_by: string | null = null
|
||||
|
||||
@createPsqlIndexStatementHelper({
|
||||
tableName: TABLE_NAME,
|
||||
columns: "deleted_at",
|
||||
where: "deleted_at IS NOT NULL",
|
||||
}).MikroORMIndex()
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date | null = null
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "txreg")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "txreg")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MikroOrmBaseRepository, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleExports } from "@medusajs/types"
|
||||
import * as Models from "@models"
|
||||
import * as ModuleModels from "@models"
|
||||
import * as ModuleServices from "@services"
|
||||
import { TaxModuleService } from "@services"
|
||||
import loadProviders from "./loaders/providers"
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName: Modules.TAX,
|
||||
models: Models,
|
||||
pathToMigrations: __dirname + "/migrations",
|
||||
}
|
||||
|
||||
const runMigrations = ModulesSdkUtils.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: ModuleModels,
|
||||
moduleRepositories: { BaseRepository: MikroOrmBaseRepository },
|
||||
moduleServices: ModuleServices,
|
||||
})
|
||||
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleName: Modules.TAX,
|
||||
moduleModels: Object.values(Models),
|
||||
migrationsPath: __dirname + "/migrations",
|
||||
})
|
||||
|
||||
const service = TaxModuleService
|
||||
const loaders = [containerLoader, connectionLoader, loadProviders] as any
|
||||
|
||||
export const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
revertMigration,
|
||||
runMigrations,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SystemTaxProvider } from "./system"
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ITaxProvider, TaxTypes } from "@medusajs/types"
|
||||
|
||||
export default class SystemTaxService implements ITaxProvider {
|
||||
static identifier = "system"
|
||||
|
||||
getIdentifier(): string {
|
||||
return SystemTaxService.identifier
|
||||
}
|
||||
|
||||
async getTaxLines(
|
||||
itemLines: TaxTypes.ItemTaxCalculationLine[],
|
||||
shippingLines: TaxTypes.ShippingTaxCalculationLine[],
|
||||
_: TaxTypes.TaxCalculationContext
|
||||
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
|
||||
let taxLines: (TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[] =
|
||||
itemLines.flatMap((l) => {
|
||||
return l.rates.map((r) => ({
|
||||
rate_id: r.id,
|
||||
rate: r.rate || 0,
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
line_item_id: l.line_item.id,
|
||||
provider_id: this.getIdentifier(),
|
||||
}))
|
||||
})
|
||||
|
||||
taxLines = taxLines.concat(
|
||||
shippingLines.flatMap((l) => {
|
||||
return l.rates.map((r) => ({
|
||||
rate_id: r.id,
|
||||
rate: r.rate || 0,
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
shipping_line_id: l.shipping_line.id,
|
||||
provider_id: this.getIdentifier(),
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
return taxLines
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import * as Models from "@models"
|
||||
import { EOL } from "os"
|
||||
|
||||
const args = process.argv
|
||||
const path = args.pop() as string
|
||||
|
||||
export default (async () => {
|
||||
const { config } = await import("dotenv")
|
||||
config()
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
`filePath is required.${EOL}Example: medusa-tax-seed <filePath>`
|
||||
)
|
||||
}
|
||||
|
||||
const run = ModulesSdkUtils.buildSeedScript({
|
||||
moduleName: Modules.TAX,
|
||||
models: Models,
|
||||
pathToMigrations: __dirname + "/../../migrations",
|
||||
seedHandler: async ({ manager, data }) => {
|
||||
// TODO: Add seed logic
|
||||
},
|
||||
})
|
||||
await run({ path })
|
||||
})()
|
||||
@@ -0,0 +1,5 @@
|
||||
describe("noop", function () {
|
||||
it("should run", function () {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TaxModuleService } from "./tax-module-service"
|
||||
@@ -0,0 +1,750 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
ITaxModuleService,
|
||||
ITaxProvider,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
TaxRegionDTO,
|
||||
TaxTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
isDefined,
|
||||
isString,
|
||||
promiseAll,
|
||||
} from "@medusajs/utils"
|
||||
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
taxRateService: ModulesSdkTypes.InternalModuleService<any>
|
||||
taxRegionService: ModulesSdkTypes.InternalModuleService<any>
|
||||
taxRateRuleService: ModulesSdkTypes.InternalModuleService<any>
|
||||
taxProviderService: ModulesSdkTypes.InternalModuleService<any>
|
||||
[key: `tp_${string}`]: ITaxProvider
|
||||
}
|
||||
|
||||
const generateForModels = [TaxRegion, TaxRateRule, TaxProvider]
|
||||
|
||||
type ItemWithRates = {
|
||||
rates: TaxRate[]
|
||||
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
|
||||
}
|
||||
|
||||
export default class TaxModuleService<
|
||||
TTaxRate extends TaxRate = TaxRate,
|
||||
TTaxRegion extends TaxRegion = TaxRegion,
|
||||
TTaxRateRule extends TaxRateRule = TaxRateRule,
|
||||
TTaxProvider extends TaxProvider = TaxProvider
|
||||
>
|
||||
extends ModulesSdkUtils.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
TaxTypes.TaxRateDTO,
|
||||
{
|
||||
TaxRegion: { dto: TaxTypes.TaxRegionDTO }
|
||||
TaxRateRule: { dto: TaxTypes.TaxRateRuleDTO }
|
||||
TaxProvider: { dto: TaxTypes.TaxProviderDTO }
|
||||
}
|
||||
>(TaxRate, generateForModels, entityNameToLinkableKeysMap)
|
||||
implements ITaxModuleService
|
||||
{
|
||||
protected readonly container_: InjectedDependencies
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected taxRateService_: ModulesSdkTypes.InternalModuleService<TTaxRate>
|
||||
protected taxRegionService_: ModulesSdkTypes.InternalModuleService<TTaxRegion>
|
||||
protected taxRateRuleService_: ModulesSdkTypes.InternalModuleService<TTaxRateRule>
|
||||
protected taxProviderService_: ModulesSdkTypes.InternalModuleService<TTaxProvider>
|
||||
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
taxRateService,
|
||||
taxRegionService,
|
||||
taxRateRuleService,
|
||||
taxProviderService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
|
||||
this.container_ = arguments[0]
|
||||
this.baseRepository_ = baseRepository
|
||||
this.taxRateService_ = taxRateService
|
||||
this.taxRegionService_ = taxRegionService
|
||||
this.taxRateRuleService_ = taxRateRuleService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
async create(
|
||||
data: TaxTypes.CreateTaxRateDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO[]>
|
||||
|
||||
async create(
|
||||
data: TaxTypes.CreateTaxRateDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async create(
|
||||
data: TaxTypes.CreateTaxRateDTO[] | TaxTypes.CreateTaxRateDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
const rates = await this.create_(input, sharedContext)
|
||||
return Array.isArray(data) ? rates : rates[0]
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async create_(
|
||||
data: TaxTypes.CreateTaxRateDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const [rules, rateData] = data.reduce(
|
||||
(acc, region) => {
|
||||
const { rules, ...rest } = region
|
||||
acc[0].push(rules)
|
||||
acc[1].push(rest)
|
||||
return acc
|
||||
},
|
||||
[[], []] as [
|
||||
(Omit<TaxTypes.CreateTaxRateRuleDTO, "tax_rate_id">[] | undefined)[],
|
||||
Partial<TaxTypes.CreateTaxRegionDTO>[]
|
||||
]
|
||||
)
|
||||
|
||||
const rates = await this.taxRateService_.create(rateData, sharedContext)
|
||||
const rulesToCreate = rates
|
||||
.reduce((acc, rate, i) => {
|
||||
const rateRules = rules[i]
|
||||
if (isDefined(rateRules)) {
|
||||
acc.push(
|
||||
rateRules.map((r) => {
|
||||
return {
|
||||
...r,
|
||||
created_by: rate.created_by,
|
||||
tax_rate_id: rate.id,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return acc
|
||||
}, [] as TaxTypes.CreateTaxRateRuleDTO[][])
|
||||
.flat()
|
||||
|
||||
if (rulesToCreate.length > 0) {
|
||||
await this.taxRateRuleService_.create(rulesToCreate, sharedContext)
|
||||
}
|
||||
|
||||
return await this.baseRepository_.serialize<TaxTypes.TaxRateDTO[]>(rates, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: TaxTypes.UpdateTaxRateDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO>
|
||||
async update(
|
||||
ids: string[],
|
||||
data: TaxTypes.UpdateTaxRateDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO[]>
|
||||
async update(
|
||||
selector: TaxTypes.FilterableTaxRateProps,
|
||||
data: TaxTypes.UpdateTaxRateDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO[]>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async update(
|
||||
selector: string | string[] | TaxTypes.FilterableTaxRateProps,
|
||||
data: TaxTypes.UpdateTaxRateDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
|
||||
const rates = await this.update_(selector, data, sharedContext)
|
||||
const serialized = await this.baseRepository_.serialize<
|
||||
TaxTypes.TaxRateDTO[]
|
||||
>(rates, { populate: true })
|
||||
return isString(selector) ? serialized[0] : serialized
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async update_(
|
||||
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
|
||||
data: TaxTypes.UpdateTaxRateDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const selector =
|
||||
Array.isArray(idOrSelector) || isString(idOrSelector)
|
||||
? { id: idOrSelector }
|
||||
: idOrSelector
|
||||
|
||||
if (data.rules) {
|
||||
await this.setTaxRateRulesForTaxRates(
|
||||
idOrSelector,
|
||||
data.rules,
|
||||
data.updated_by,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
delete data.rules
|
||||
}
|
||||
|
||||
return await this.taxRateService_.update({ selector, data }, sharedContext)
|
||||
}
|
||||
|
||||
private async setTaxRateRulesForTaxRates(
|
||||
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
|
||||
rules: Omit<TaxTypes.CreateTaxRateRuleDTO, "tax_rate_id">[],
|
||||
createdBy?: string,
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
const selector =
|
||||
Array.isArray(idOrSelector) || isString(idOrSelector)
|
||||
? { id: idOrSelector }
|
||||
: idOrSelector
|
||||
|
||||
await this.taxRateRuleService_.softDelete(
|
||||
{ tax_rate: selector },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// TODO: this is a temporary solution seems like mikro-orm doesn't persist
|
||||
// the soft delete which results in the creation below breaking the unique
|
||||
// constraint
|
||||
await this.taxRateRuleService_.list(
|
||||
{ tax_rate: selector },
|
||||
{ select: ["id"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (rules.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const rateIds = await this.getTaxRateIdsFromSelector(idOrSelector)
|
||||
const toCreate = rateIds
|
||||
.map((id) => {
|
||||
return rules.map((r) => {
|
||||
return {
|
||||
...r,
|
||||
created_by: createdBy,
|
||||
tax_rate_id: id,
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
|
||||
return await this.createTaxRateRules(toCreate, sharedContext)
|
||||
}
|
||||
|
||||
private async getTaxRateIdsFromSelector(
|
||||
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
if (Array.isArray(idOrSelector)) {
|
||||
return idOrSelector
|
||||
}
|
||||
|
||||
if (isString(idOrSelector)) {
|
||||
return [idOrSelector]
|
||||
}
|
||||
|
||||
const rates = await this.taxRateService_.list(
|
||||
idOrSelector,
|
||||
{ select: ["id"] },
|
||||
sharedContext
|
||||
)
|
||||
return rates.map((r) => r.id)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
data: TaxTypes.UpsertTaxRateDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO[]>
|
||||
async upsert(
|
||||
data: TaxTypes.UpsertTaxRateDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateDTO>
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async upsert(
|
||||
data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
|
||||
const result = await this.taxRateService_.upsert(data, sharedContext)
|
||||
const serialized = await this.baseRepository_.serialize<
|
||||
TaxTypes.TaxRateDTO[]
|
||||
>(result, { populate: true })
|
||||
return Array.isArray(data) ? serialized : serialized[0]
|
||||
}
|
||||
|
||||
createTaxRegions(
|
||||
data: TaxTypes.CreateTaxRegionDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxRegionDTO>
|
||||
|
||||
createTaxRegions(
|
||||
data: TaxTypes.CreateTaxRegionDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<TaxRegionDTO[]>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async createTaxRegions(
|
||||
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
const result = await this.createTaxRegions_(input, sharedContext)
|
||||
return Array.isArray(data) ? result : result[0]
|
||||
}
|
||||
|
||||
async createTaxRegions_(
|
||||
data: TaxTypes.CreateTaxRegionDTO[],
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
const { defaultRates, regionData } =
|
||||
this.prepareTaxRegionInputForCreate(data)
|
||||
|
||||
await this.verifyProvinceToCountryMatch(regionData, sharedContext)
|
||||
|
||||
const regions = await this.taxRegionService_.create(
|
||||
regionData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const rates = regions
|
||||
.map((region, i) => {
|
||||
if (!defaultRates[i]) {
|
||||
return false
|
||||
}
|
||||
return {
|
||||
...defaultRates[i],
|
||||
tax_region_id: region.id,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as TaxTypes.CreateTaxRateDTO[]
|
||||
|
||||
if (rates.length !== 0) {
|
||||
await this.create(rates, sharedContext)
|
||||
}
|
||||
|
||||
return await this.baseRepository_.serialize<TaxTypes.TaxRegionDTO[]>(
|
||||
regions,
|
||||
{ populate: true }
|
||||
)
|
||||
}
|
||||
|
||||
createTaxRateRules(
|
||||
data: TaxTypes.CreateTaxRateRuleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateRuleDTO>
|
||||
createTaxRateRules(
|
||||
data: TaxTypes.CreateTaxRateRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<TaxTypes.TaxRateRuleDTO[]>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async createTaxRateRules(
|
||||
data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
const result = await this.createTaxRateRules_(input, sharedContext)
|
||||
return Array.isArray(data) ? result : result[0]
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async createTaxRateRules_(
|
||||
data: TaxTypes.CreateTaxRateRuleDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const rules = await this.taxRateRuleService_.create(data, sharedContext)
|
||||
return await this.baseRepository_.serialize<TaxTypes.TaxRateRuleDTO[]>(
|
||||
rules,
|
||||
{
|
||||
populate: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async getTaxLines(
|
||||
items: (TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO)[],
|
||||
calculationContext: TaxTypes.TaxCalculationContext,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
|
||||
const normalizedContext =
|
||||
this.normalizeTaxCalculationContext(calculationContext)
|
||||
const regions = await this.taxRegionService_.list(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
country_code: normalizedContext.address.country_code,
|
||||
province_code: null,
|
||||
},
|
||||
{
|
||||
country_code: normalizedContext.address.country_code,
|
||||
province_code: normalizedContext.address.province_code,
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const parentRegion = regions.find((r) => r.province_code === null)
|
||||
if (!parentRegion) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"No parent region found for country"
|
||||
)
|
||||
}
|
||||
|
||||
const toReturn = await promiseAll(
|
||||
items.map(async (item) => {
|
||||
const regionIds = regions.map((r) => r.id)
|
||||
const rateQuery = this.getTaxRateQueryForItem(item, regionIds)
|
||||
const candidateRates = await this.taxRateService_.list(
|
||||
rateQuery,
|
||||
{
|
||||
relations: ["tax_region", "rules"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const applicableRates = await this.getTaxRatesForItem(
|
||||
item,
|
||||
candidateRates
|
||||
)
|
||||
|
||||
return {
|
||||
rates: applicableRates,
|
||||
item,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const taxLines = await this.getTaxLinesFromProvider(
|
||||
parentRegion.provider_id,
|
||||
toReturn,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
return taxLines
|
||||
}
|
||||
|
||||
private async getTaxLinesFromProvider(
|
||||
rawProviderId: string | null,
|
||||
items: ItemWithRates[],
|
||||
calculationContext: TaxTypes.TaxCalculationContext
|
||||
) {
|
||||
const providerId = rawProviderId || "system"
|
||||
let provider: ITaxProvider
|
||||
try {
|
||||
provider = this.container_[`tp_${providerId}`] as ITaxProvider
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Failed to resolve Tax Provider with id: ${providerId}. Make sure it's installed and configured in the Tax Module's options.`
|
||||
)
|
||||
}
|
||||
|
||||
const [itemLines, shippingLines] = items.reduce(
|
||||
(acc, line) => {
|
||||
if ("shipping_option_id" in line.item) {
|
||||
acc[1].push({
|
||||
shipping_line: line.item,
|
||||
rates: line.rates,
|
||||
})
|
||||
} else {
|
||||
acc[0].push({
|
||||
line_item: line.item,
|
||||
rates: line.rates,
|
||||
})
|
||||
}
|
||||
return acc
|
||||
},
|
||||
[[], []] as [
|
||||
TaxTypes.ItemTaxCalculationLine[],
|
||||
TaxTypes.ShippingTaxCalculationLine[]
|
||||
]
|
||||
)
|
||||
|
||||
const itemTaxLines = await provider.getTaxLines(
|
||||
itemLines,
|
||||
shippingLines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
return itemTaxLines
|
||||
}
|
||||
|
||||
private normalizeTaxCalculationContext(
|
||||
context: TaxTypes.TaxCalculationContext
|
||||
): TaxTypes.TaxCalculationContext {
|
||||
return {
|
||||
...context,
|
||||
address: {
|
||||
...context.address,
|
||||
country_code: this.normalizeRegionCodes(context.address.country_code),
|
||||
province_code: context.address.province_code
|
||||
? this.normalizeRegionCodes(context.address.province_code)
|
||||
: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private prepareTaxRegionInputForCreate(
|
||||
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[]
|
||||
) {
|
||||
const regionsWithDefaultRate = Array.isArray(data) ? data : [data]
|
||||
|
||||
const defaultRates: (Omit<
|
||||
TaxTypes.CreateTaxRateDTO,
|
||||
"tax_region_id"
|
||||
> | null)[] = []
|
||||
const regionData: TaxTypes.CreateTaxRegionDTO[] = []
|
||||
|
||||
for (const region of regionsWithDefaultRate) {
|
||||
const { default_tax_rate, ...rest } = region
|
||||
if (!default_tax_rate) {
|
||||
defaultRates.push(null)
|
||||
} else {
|
||||
defaultRates.push({
|
||||
...default_tax_rate,
|
||||
is_default: true,
|
||||
created_by: region.created_by,
|
||||
})
|
||||
}
|
||||
|
||||
regionData.push({
|
||||
...rest,
|
||||
province_code: rest.province_code
|
||||
? this.normalizeRegionCodes(rest.province_code)
|
||||
: null,
|
||||
country_code: this.normalizeRegionCodes(rest.country_code),
|
||||
})
|
||||
}
|
||||
|
||||
return { defaultRates, regionData }
|
||||
}
|
||||
|
||||
private async verifyProvinceToCountryMatch(
|
||||
regionsToVerify: TaxTypes.CreateTaxRegionDTO[],
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
const parentIds = regionsToVerify.map((i) => i.parent_id).filter(isDefined)
|
||||
if (parentIds.length > 0) {
|
||||
const parentRegions = await this.taxRegionService_.list(
|
||||
{ id: { $in: parentIds } },
|
||||
{ select: ["id", "country_code"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
for (const region of regionsToVerify) {
|
||||
if (isDefined(region.parent_id)) {
|
||||
const parentRegion = parentRegions.find(
|
||||
(r) => r.id === region.parent_id
|
||||
)
|
||||
if (!isDefined(parentRegion)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Province region must belong to a parent region. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent does not exist`
|
||||
)
|
||||
}
|
||||
|
||||
if (parentRegion.country_code !== region.country_code) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Province region must belong to a parent region with the same country code. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent expects (country: ${parentRegion.country_code})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getTaxRatesForItem(
|
||||
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
|
||||
rates: TTaxRate[]
|
||||
): Promise<TTaxRate[]> {
|
||||
if (!rates.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const prioritizedRates = this.prioritizeRates(rates, item)
|
||||
const rate = prioritizedRates[0]
|
||||
|
||||
const ratesToReturn = [rate]
|
||||
|
||||
// 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(parentRate)
|
||||
}
|
||||
|
||||
return ratesToReturn
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private normalizeRegionCodes(code: string) {
|
||||
return code.toLowerCase()
|
||||
}
|
||||
|
||||
// @InjectTransactionManager("baseRepository_")
|
||||
// async createProvidersOnLoad(@MedusaContext() sharedContext: Context = {}) {
|
||||
// const providersToLoad = this.container_["tax_providers"] as ITaxProvider[]
|
||||
|
||||
// const ids = providersToLoad.map((p) => p.getIdentifier())
|
||||
|
||||
// const existing = await this.taxProviderService_.update(
|
||||
// { selector: { id: { $in: ids } }, data: { is_enabled: true } },
|
||||
// sharedContext
|
||||
// )
|
||||
|
||||
// const existingIds = existing.map((p) => p.id)
|
||||
// const diff = arrayDifference(ids, existingIds)
|
||||
// await this.taxProviderService_.create(
|
||||
// diff.map((id) => ({ id, is_enabled: true }))
|
||||
// )
|
||||
|
||||
// await this.taxProviderService_.update({
|
||||
// selector: { id: { $nin: ids } },
|
||||
// data: { is_enabled: false },
|
||||
// })
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2020"],
|
||||
"target": "es2020",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": false,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true, // to use ES5 specific tooling
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@models": ["./src/models"],
|
||||
"@services": ["./src/services"],
|
||||
"@repositories": ["./src/repositories"],
|
||||
"@types": ["./src/types"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"./src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src", "integration-tests"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user