feat(medusa): Tax-inclusive pricing (#2131)
* add feature flag for tax inclusive pricing * update db model for TIP * add migration * set featureflag column decorators * remove unused prop * update tests to reflect feature_flags as any array * fix types * reference key from featureFlag file * use feature flag key in models * fix copy paste mistake * unify spelling * Create gorgeous-experts-guess.md * feat(medusa): create/update endpoints of currency/region/price-lists/shipping-options should allow to pass includes_tax * test(integration): continue to add some integration test * test(integration): continue to add some integration test * test(unit): Fix region service tests * fix(medusa): API unit tests flags management * feat(medusa): Minor cleanup * style(medusa): Fix typo * fix(medusa): rebase * feat(medusa): Replace old tag with the new one * feat(medusa): revert flag * feat(medusa): Cleanup * feat(medusa): feedback * feat(medusa): Rename currency retrieve method * test(medudsa): fix unit tests * chore(medusa): fix oas * feat(medusa): ShippingMethod should include tax setting from parent option (#2021) * feat(medusa): Shipping method should includes tax from parent options * feat(medusa): Condition the includes tax flag to the availability of the feature and add some other tests * test(integration): Move cart/order ff test in separate files * fix: snapshots folder * fix(integration): snapshots * Create calm-baboons-sit.md * test(integration): file naming Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com> * Feat/tax inclusive pricing extend price selection strategy (#2087) * initial changes to price selection strategy including unit tests * typing for tax calculation * update types and remove region and currency from prices results * fix casing * include tax calculation in priceselectionstrategy * integration tests for tax inclusive pricing price calculations * fix build * include tax inclusive considerations when calculating tax fields for variants * include only "includes_tax" fields from currency and region joins * test to see errors in pipelines * conditionally join featureflagged fields * add "includes_tax" to price list factory * add tests for tax inclusive price list prices and currency prices * fix unit tests * refactor pricing array checks to expect arraycontaining * undo error handler * Feat/tax inclusive pricing flag on generated lineitems (#2108) * include tax inclusive pricing flag on generated lineitems * initial addition of tax inclusivity for lineitem service * add generate test to ensure that includes_tax is set when returned from price selection strategy * add integration test for generating lineitem including tax * add test for negative tax inclusion * add tests for mixed pricing * add negative test for setting tax exclusivity * restructure the setting of includes_tax on lineitems * fix: update cwd to be correct in cart test * feat(medusa): Line item totals calculations (#2123) * feat(medusa): Update totals and tax calculation way to calculate the totals * feat(medusa): remove region feetching from decorate total * feat(medusa): cleanup * test(medusa): fix tax calculation tests * comment * test(integration): cleanup * test(integration): cleanup * fix(medusa): return service missing await * feat(medusa): cleanup * feat(medusa): cleanup * test(integration): fix data * feat(medusa): improve tax calculation readability * test(medusa): improve tax calculation structure case Co-authored-by: Sebastian Rindom <skrindom@gmail.com> * Feat(medusa): tax inclusive pricing in shipping method tax (#2125) * initial implementation and test * include tax inclusive calculations for getting shipping options * remove inaccurate comment * remove console log * refactor how prices and taxes are set for shipping methods * fix integration tests * remove verbose flag * fix integration tests * remove console log * format util * use util in price service and tax strategy * fix faulty integration test * undo tax calculation strategy changes in favor or Carlos' pr * undo changes to tax calculation strategy tests * round tax amount * feat(medusa): cleanup calculate tax amount utils and its usage (#2136) * feat(medusa): Refund line totals calculation (#2139) Rely on the update of the following pr https://github.com/medusajs/medusa/pull/2136 **WIP Missing integration tests** **What** Update the totals calculation on the refund line to include the notion of tax inclusive **Test** - Update and add new tests around the refund Fixes CORE-482 * feat(medusa): Tax inclusive discounts calculations (#2137) **What** - Calculate line adjustments correctly taking into account the tax inclusivity - fix totals getLineItemTotals by adjusting the sub total with the original tax amount instead of the tax amount when the unit price includes the taxes **Tests** - The tests create a cart with a percentage discount of 15%, the cart includes 2 items mixing the tax inclusive and validate the items on the result cart as well as the totals on each item. I ve based my calculation validation based on what we have done + some articles around discount apply on price without taxes to validate the output., FIXES CORE-477 * Chore: shipping methods tax inclusive total (#2130) * chore: calculate tax inclusive shipping methods * chore: additional tests and check undefined tax_rate (#2157) * chore: additional tests and check undefined tax_rate * fix: naming + correct price type check * fix: remove price_includes_tax from type * fix: remove price_includes_tax from type Co-authored-by: Philip Korsholm <philip.korsholm@hotmail.com> Co-authored-by: adrien2p <adrien.deperetti@gmail.com> Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com> Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Sebastian Rindom <skrindom@gmail.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Philip Korsholm
adrien2p
Carlos R. L. Rodrigues
Philip Korsholm
Sebastian Rindom
Carlos R. L. Rodrigues
parent
af80e0fd2e
commit
cbb7dd9787
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Pass down the includes_tax to the shipping method from the shipping option
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Extend models Currency, Region, PriceList, ShippingOption, LineItem, ShippingMethod with tax inclusive flag
|
||||
@@ -0,0 +1,163 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`/admin/currencies GET /admin/currencies should retrieve the currencies 1`] = `
|
||||
Object {
|
||||
"count": 120,
|
||||
"currencies": Array [
|
||||
Object {
|
||||
"code": "aed",
|
||||
"includes_tax": false,
|
||||
"name": "United Arab Emirates Dirham",
|
||||
"symbol": "AED",
|
||||
"symbol_native": "د.إ.",
|
||||
},
|
||||
Object {
|
||||
"code": "afn",
|
||||
"includes_tax": false,
|
||||
"name": "Afghan Afghani",
|
||||
"symbol": "Af",
|
||||
"symbol_native": "؋",
|
||||
},
|
||||
Object {
|
||||
"code": "all",
|
||||
"includes_tax": false,
|
||||
"name": "Albanian Lek",
|
||||
"symbol": "ALL",
|
||||
"symbol_native": "Lek",
|
||||
},
|
||||
Object {
|
||||
"code": "amd",
|
||||
"includes_tax": false,
|
||||
"name": "Armenian Dram",
|
||||
"symbol": "AMD",
|
||||
"symbol_native": "դր.",
|
||||
},
|
||||
Object {
|
||||
"code": "ars",
|
||||
"includes_tax": false,
|
||||
"name": "Argentine Peso",
|
||||
"symbol": "AR$",
|
||||
"symbol_native": "$",
|
||||
},
|
||||
Object {
|
||||
"code": "aud",
|
||||
"includes_tax": false,
|
||||
"name": "Australian Dollar",
|
||||
"symbol": "AU$",
|
||||
"symbol_native": "$",
|
||||
},
|
||||
Object {
|
||||
"code": "azn",
|
||||
"includes_tax": false,
|
||||
"name": "Azerbaijani Manat",
|
||||
"symbol": "man.",
|
||||
"symbol_native": "ман.",
|
||||
},
|
||||
Object {
|
||||
"code": "bam",
|
||||
"includes_tax": false,
|
||||
"name": "Bosnia-Herzegovina Convertible Mark",
|
||||
"symbol": "KM",
|
||||
"symbol_native": "KM",
|
||||
},
|
||||
Object {
|
||||
"code": "bdt",
|
||||
"includes_tax": false,
|
||||
"name": "Bangladeshi Taka",
|
||||
"symbol": "Tk",
|
||||
"symbol_native": "৳",
|
||||
},
|
||||
Object {
|
||||
"code": "bgn",
|
||||
"includes_tax": false,
|
||||
"name": "Bulgarian Lev",
|
||||
"symbol": "BGN",
|
||||
"symbol_native": "лв.",
|
||||
},
|
||||
Object {
|
||||
"code": "bhd",
|
||||
"includes_tax": false,
|
||||
"name": "Bahraini Dinar",
|
||||
"symbol": "BD",
|
||||
"symbol_native": "د.ب.",
|
||||
},
|
||||
Object {
|
||||
"code": "bif",
|
||||
"includes_tax": false,
|
||||
"name": "Burundian Franc",
|
||||
"symbol": "FBu",
|
||||
"symbol_native": "FBu",
|
||||
},
|
||||
Object {
|
||||
"code": "bnd",
|
||||
"includes_tax": false,
|
||||
"name": "Brunei Dollar",
|
||||
"symbol": "BN$",
|
||||
"symbol_native": "$",
|
||||
},
|
||||
Object {
|
||||
"code": "bob",
|
||||
"includes_tax": false,
|
||||
"name": "Bolivian Boliviano",
|
||||
"symbol": "Bs",
|
||||
"symbol_native": "Bs",
|
||||
},
|
||||
Object {
|
||||
"code": "brl",
|
||||
"includes_tax": false,
|
||||
"name": "Brazilian Real",
|
||||
"symbol": "R$",
|
||||
"symbol_native": "R$",
|
||||
},
|
||||
Object {
|
||||
"code": "bwp",
|
||||
"includes_tax": false,
|
||||
"name": "Botswanan Pula",
|
||||
"symbol": "BWP",
|
||||
"symbol_native": "P",
|
||||
},
|
||||
Object {
|
||||
"code": "byn",
|
||||
"includes_tax": false,
|
||||
"name": "Belarusian Ruble",
|
||||
"symbol": "Br",
|
||||
"symbol_native": "руб.",
|
||||
},
|
||||
Object {
|
||||
"code": "bzd",
|
||||
"includes_tax": false,
|
||||
"name": "Belize Dollar",
|
||||
"symbol": "BZ$",
|
||||
"symbol_native": "$",
|
||||
},
|
||||
Object {
|
||||
"code": "cad",
|
||||
"includes_tax": false,
|
||||
"name": "Canadian Dollar",
|
||||
"symbol": "CA$",
|
||||
"symbol_native": "$",
|
||||
},
|
||||
Object {
|
||||
"code": "cdf",
|
||||
"includes_tax": false,
|
||||
"name": "Congolese Franc",
|
||||
"symbol": "CDF",
|
||||
"symbol_native": "FrCD",
|
||||
},
|
||||
],
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`/admin/currencies POST /admin/currencies/:code should update currency includes_tax 1`] = `
|
||||
Object {
|
||||
"currency": Object {
|
||||
"code": "aed",
|
||||
"includes_tax": true,
|
||||
"name": "United Arab Emirates Dirham",
|
||||
"symbol": "AED",
|
||||
"symbol_native": "د.إ.",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -121,12 +121,7 @@ Object {
|
||||
"symbol_native": "$",
|
||||
},
|
||||
"default_currency_code": "usd",
|
||||
"feature_flags": Array [
|
||||
Object {
|
||||
"key": "sales_channels",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"feature_flags": Any<Array>,
|
||||
"fulfillment_providers": Array [
|
||||
Object {
|
||||
"id": "test-ful",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const path = require("path")
|
||||
const startServerWithEnvironment =
|
||||
require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { useDb } = require("../../../helpers/use-db")
|
||||
const adminSeeder = require("../../helpers/admin-seeder");
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
describe("/admin/currencies", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("GET /admin/currencies", function () {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should retrieve the currencies", async () => {
|
||||
const api = useApi()
|
||||
const response = await api.get(
|
||||
`/admin/currencies?order=code`,
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(response.data).toMatchSnapshot()
|
||||
})
|
||||
});
|
||||
|
||||
describe("POST /admin/currencies/:code", function () {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should update currency includes_tax", async () => {
|
||||
const api = useApi()
|
||||
const response = await api.post(
|
||||
`/admin/currencies/aed`,
|
||||
{
|
||||
includes_tax: true
|
||||
},
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(response.data).toMatchSnapshot()
|
||||
})
|
||||
});
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
const path = require("path")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||
|
||||
const {
|
||||
simpleRegionFactory,
|
||||
simpleShippingOptionFactory,
|
||||
simpleOrderFactory
|
||||
} = require("../../../factories");
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/orders", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /admin/orders/:id/shipping-methods", () => {
|
||||
let includesTaxShippingOption
|
||||
let order
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
const shippingAddress = {
|
||||
id: "test-shipping-address",
|
||||
first_name: "lebron",
|
||||
country_code: "us",
|
||||
}
|
||||
const region = await simpleRegionFactory(dbConnection, {
|
||||
id: "test-region"
|
||||
})
|
||||
order = await simpleOrderFactory(dbConnection, {
|
||||
id: "test-order",
|
||||
region: region.id,
|
||||
shipping_address: shippingAddress,
|
||||
currency_code: "usd",
|
||||
})
|
||||
includesTaxShippingOption = await simpleShippingOptionFactory(dbConnection, {
|
||||
includes_tax: true,
|
||||
region_id: region.id
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async() => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("should add a normal shipping method to the order", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const orderWithShippingMethodRes = await api.post(
|
||||
`/admin/orders/${order.id}/shipping-methods`,
|
||||
{
|
||||
option_id: includesTaxShippingOption.id,
|
||||
price: 10,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(orderWithShippingMethodRes.status).toEqual(200)
|
||||
expect(orderWithShippingMethodRes.data.order.shipping_methods)
|
||||
.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
shipping_option_id: includesTaxShippingOption.id,
|
||||
includes_tax: true,
|
||||
})
|
||||
]))
|
||||
})
|
||||
})
|
||||
})
|
||||
+9
-9
@@ -7,21 +7,21 @@ const {
|
||||
ShippingMethod,
|
||||
} = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const setupServer = require("../../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const orderSeeder = require("../../helpers/order-seeder")
|
||||
const swapSeeder = require("../../helpers/swap-seeder")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const claimSeeder = require("../../helpers/claim-seeder")
|
||||
const orderSeeder = require("../../../helpers/order-seeder")
|
||||
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||
const claimSeeder = require("../../../helpers/claim-seeder")
|
||||
|
||||
const {
|
||||
expectPostCallToReturn,
|
||||
expectAllPostCallsToReturn,
|
||||
callGet,
|
||||
partial,
|
||||
} = require("../../helpers/call-helpers")
|
||||
} = require("../../../helpers/call-helpers")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("/admin/orders", () => {
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd })
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
const { PriceList, CustomerGroup } = require("@medusajs/medusa")
|
||||
const path = require("path")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { useDb, initDb } = require("../../../helpers/use-db")
|
||||
|
||||
@@ -9,14 +9,17 @@ const {
|
||||
simpleProductFactory,
|
||||
simplePriceListFactory,
|
||||
} = require("../../factories")
|
||||
const {
|
||||
simpleCustomerGroupFactory,
|
||||
} = require("../../factories/simple-customer-group-factory")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const customerSeeder = require("../../helpers/customer-seeder")
|
||||
const priceListSeeder = require("../../helpers/price-list-seeder")
|
||||
const productSeeder = require("../../helpers/product-seeder")
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("/admin/price-lists", () => {
|
||||
@@ -1141,54 +1144,52 @@ describe("/admin/price-lists", () => {
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(2)
|
||||
expect(response.data.products).toHaveLength(2)
|
||||
expect(response.data.products).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "test-prod-1",
|
||||
variants: [
|
||||
expect.objectContaining({
|
||||
id: "test-variant-1",
|
||||
prices: [
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
amount: 150,
|
||||
price_list_id: "test-list",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-variant-2",
|
||||
prices: [
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-prod-2",
|
||||
variants: [
|
||||
expect.objectContaining({
|
||||
id: "test-variant-3",
|
||||
prices: [
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-variant-4",
|
||||
prices: [
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
amount: 150,
|
||||
price_list_id: "test-list",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(response.data.products).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-prod-1",
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "test-variant-1",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
amount: 150,
|
||||
price_list_id: "test-list",
|
||||
}),
|
||||
],)
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-variant-2",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-prod-2",
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "test-variant-3",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-variant-4",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
amount: 150,
|
||||
price_list_id: "test-list",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("lists only product 2", async () => {
|
||||
@@ -1387,3 +1388,112 @@ describe("/admin/price-lists", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/price-lists", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /admin/price-list", () => {
|
||||
const priceListIncludesTaxId = "price-list-1-includes-tax"
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
await customerSeeder(dbConnection)
|
||||
await productSeeder(dbConnection)
|
||||
await simplePriceListFactory(dbConnection, {
|
||||
id: priceListIncludesTaxId,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should creates a price list that includes tax", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "VIP Summer sale",
|
||||
description: "Summer sale for VIP customers. 25% off selected items.",
|
||||
type: "sale",
|
||||
status: "active",
|
||||
starts_at: "2022-07-01T00:00:00.000Z",
|
||||
ends_at: "2022-07-31T00:00:00.000Z",
|
||||
customer_groups: [
|
||||
{
|
||||
id: "customer-group-1",
|
||||
},
|
||||
],
|
||||
prices: [
|
||||
{
|
||||
amount: 85,
|
||||
currency_code: "usd",
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
],
|
||||
includes_tax: true,
|
||||
}
|
||||
|
||||
const response = await api
|
||||
.post("/admin/price-lists", payload, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.warn(err.response.data)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.price_list).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
includes_tax: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update a price list that include_tax", async () => {
|
||||
const api = useApi()
|
||||
|
||||
let response = await api
|
||||
.get(`/admin/price-lists/${priceListIncludesTaxId}`, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.price_list.includes_tax).toBe(false)
|
||||
|
||||
response = await api
|
||||
.post(
|
||||
`/admin/price-lists/${priceListIncludesTaxId}`,
|
||||
{ includes_tax: true, },
|
||||
adminReqConfig
|
||||
).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.price_list.includes_tax).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,17 @@ const path = require("path")
|
||||
const { Region } = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const { simpleRegionFactory } = require("../../factories");
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -286,3 +294,99 @@ describe("/admin/regions", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /admin/regions/:id", () => {
|
||||
const region1TaxInclusiveId = "region-1-tax-inclusive"
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
await simpleRegionFactory(dbConnection, {
|
||||
id: region1TaxInclusiveId,
|
||||
countries: ["fr"],
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should allow to create a region that includes tax", async function () {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "region-including-taxes",
|
||||
currency_code: "usd",
|
||||
tax_rate: 0,
|
||||
payment_providers: ["test-pay"],
|
||||
fulfillment_providers: ["test-ful"],
|
||||
countries: ["us"],
|
||||
includes_tax: true,
|
||||
}
|
||||
|
||||
let response = await api
|
||||
.post(`/admin/regions`, payload, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.region).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
includes_tax: true,
|
||||
name: "region-including-taxes",
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
it("should allow to update a region that includes tax", async function () {
|
||||
const api = useApi()
|
||||
let response = await api
|
||||
.get(`/admin/regions/${region1TaxInclusiveId}`, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.region.includes_tax).toBe(false)
|
||||
|
||||
response = await api.post(
|
||||
`/admin/regions/${region1TaxInclusiveId}`,
|
||||
{
|
||||
includes_tax: true,
|
||||
},
|
||||
adminReqConfig,
|
||||
).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.region.includes_tax).toBe(true)
|
||||
});
|
||||
})
|
||||
})
|
||||
@@ -1,16 +1,21 @@
|
||||
const path = require("path")
|
||||
const {
|
||||
Region,
|
||||
ShippingProfile,
|
||||
ShippingOption,
|
||||
ShippingOptionRequirement,
|
||||
} = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const shippingOptionSeeder = require("../../helpers/shipping-option-seeder")
|
||||
const { simpleShippingOptionFactory, simpleRegionFactory } = require("../../factories")
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -460,3 +465,120 @@ describe("/admin/shipping-options", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /admin/shipping-options", () => {
|
||||
const shippingOptionIncludesTaxId = "shipping-option-1-includes-tax"
|
||||
let region
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
region = await simpleRegionFactory(dbConnection, {
|
||||
id: "region",
|
||||
countries: ["fr"],
|
||||
})
|
||||
await simpleShippingOptionFactory(dbConnection, {
|
||||
id: shippingOptionIncludesTaxId,
|
||||
region_id: region.id,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should creates a shipping option that includes tax", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const defaultProfile = await dbConnection.manager.findOne(ShippingProfile, {
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const payload = {
|
||||
name: "Test option",
|
||||
amount: 100,
|
||||
price_type: "flat_rate",
|
||||
region_id: region.id,
|
||||
provider_id: "test-ful",
|
||||
data: {},
|
||||
profile_id: defaultProfile.id,
|
||||
includes_tax: true,
|
||||
}
|
||||
|
||||
const response = await api
|
||||
.post("/admin/shipping-options", payload, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.shipping_option).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
includes_tax: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update a shipping option that include_tax", async () => {
|
||||
const api = useApi()
|
||||
|
||||
let response = await api
|
||||
.get(`/admin/shipping-options/${shippingOptionIncludesTaxId}`, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.shipping_option.includes_tax).toBe(false)
|
||||
|
||||
const payload = {
|
||||
requirements: [
|
||||
{
|
||||
type: "min_subtotal",
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
type: "max_subtotal",
|
||||
amount: 2,
|
||||
},
|
||||
],
|
||||
includes_tax: true,
|
||||
}
|
||||
|
||||
response = await api
|
||||
.post(`/admin/shipping-options/${shippingOptionIncludesTaxId}`, payload, adminReqConfig)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(response.data.shipping_option.includes_tax).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,12 +52,7 @@ describe("/admin/store", () => {
|
||||
code: "usd",
|
||||
},
|
||||
],
|
||||
feature_flags: [
|
||||
{
|
||||
key: "sales_channels",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
feature_flags: expect.any(Array),
|
||||
default_currency_code: "usd",
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
@@ -133,15 +128,17 @@ describe("/admin/store", () => {
|
||||
it("successfully updates default currency code", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
"/admin/store",
|
||||
{
|
||||
default_currency_code: "dkk",
|
||||
},
|
||||
{
|
||||
headers: { Authorization: "Bearer test_token " },
|
||||
}
|
||||
)
|
||||
const response = await api
|
||||
.post(
|
||||
"/admin/store",
|
||||
{
|
||||
default_currency_code: "dkk",
|
||||
},
|
||||
{
|
||||
headers: { Authorization: "Bearer test_token " },
|
||||
}
|
||||
)
|
||||
.catch((err) => console.log(err))
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.store).toMatchSnapshot({
|
||||
|
||||
@@ -250,12 +250,10 @@ describe("/admin/swaps", () => {
|
||||
data: {},
|
||||
})
|
||||
await api.post("/store/carts/cart-test/payment-sessions")
|
||||
const TEST = await api.post("/store/carts/cart-test/payment-session", {
|
||||
await api.post("/store/carts/cart-test/payment-session", {
|
||||
provider_id: "test-pay",
|
||||
})
|
||||
|
||||
console.log("Testing, ", TEST.data.cart.items[0])
|
||||
|
||||
// ********* COMPLETE CART *********
|
||||
const completedOrder = await api.post("/store/carts/cart-test/complete")
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ describe("Line Item Adjustments", () => {
|
||||
})
|
||||
|
||||
describe("Tests database constraints", () => {
|
||||
let cart,
|
||||
discount,
|
||||
lineItemId = "line-test"
|
||||
let cart
|
||||
let discount
|
||||
const lineItemId = "line-test"
|
||||
beforeEach(async () => {
|
||||
await cartSeeder(dbConnection)
|
||||
discount = await simpleDiscountFactory(dbConnection, {
|
||||
@@ -113,7 +113,7 @@ describe("Line Item Adjustments", () => {
|
||||
})
|
||||
}
|
||||
|
||||
expect(createLineItemWithAdjustment()).resolves.toEqual(
|
||||
await expect(createLineItemWithAdjustment()).resolves.toEqual(
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
@@ -131,7 +131,7 @@ describe("Line Item Adjustments", () => {
|
||||
})
|
||||
}
|
||||
|
||||
expect(createAdjustmentNullDiscount()).resolves.toEqual(
|
||||
await expect(createAdjustmentNullDiscount()).resolves.toEqual(
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
@@ -155,7 +155,8 @@ describe("Line Item Adjustments", () => {
|
||||
discount_id: null,
|
||||
})
|
||||
}
|
||||
expect(createAdjustmentsNullDiscount()).resolves.toEqual(
|
||||
|
||||
await expect(createAdjustmentsNullDiscount()).resolves.toEqual(
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
@@ -184,7 +185,7 @@ describe("Line Item Adjustments", () => {
|
||||
})
|
||||
}
|
||||
|
||||
expect(createAdjustment()).resolves.toEqual(expect.anything())
|
||||
await expect(createAdjustment()).resolves.toEqual(expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -199,7 +200,7 @@ describe("Line Item Adjustments", () => {
|
||||
discount_id: discount.id,
|
||||
})
|
||||
|
||||
expect(createDuplicateAdjustment()).rejects.toEqual(
|
||||
await expect(createDuplicateAdjustment()).rejects.toEqual(
|
||||
expect.objectContaining({ code: "23505" })
|
||||
)
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,276 @@
|
||||
const path = require("path")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { useDb } = require("../../../helpers/use-db")
|
||||
|
||||
const { simpleProductFactory, simpleOrderFactory } = require("../../factories")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
|
||||
const createReturnableOrder = async (dbConnection, options) => {
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-product",
|
||||
variants: [{ id: "test-variant" }],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
let discounts = []
|
||||
|
||||
if (options?.discount) {
|
||||
discounts = [
|
||||
{
|
||||
code: "TESTCODE",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
let unitPrice = options.includes_tax ? 1200 : 1000
|
||||
if (options.oldTaxes) {
|
||||
unitPrice = options.includes_tax ? 1125 : 1000
|
||||
}
|
||||
|
||||
return await simpleOrderFactory(dbConnection, {
|
||||
email: "test@testson.com",
|
||||
tax_rate: options?.oldTaxes ? undefined : null,
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12.5,
|
||||
},
|
||||
discounts,
|
||||
line_items: [
|
||||
{
|
||||
id: "test-item",
|
||||
variant_id: "test-variant",
|
||||
quantity: 2,
|
||||
fulfilled_quantity: options?.shipped ? 2 : undefined,
|
||||
shipped_quantity: options?.shipped ? 2 : undefined,
|
||||
unit_price: unitPrice,
|
||||
includes_tax: options?.includes_tax,
|
||||
tax_lines: [
|
||||
{
|
||||
name: "default",
|
||||
code: "default",
|
||||
rate: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("creates a return with the old tax system and tax inclusive line", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
oldTaxes: true,
|
||||
includes_tax: true,
|
||||
})
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/orders/${order.id}/return`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has default tax rate 12.5 therefore refund amount should be
|
||||
* 1000 * 1.125 = 1125
|
||||
*/
|
||||
expect(response.data.order.returns[0].refund_amount).toEqual(1125)
|
||||
expect(response.data.order.returns[0].items).toHaveLength(1)
|
||||
expect(response.data.order.returns[0].items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a return with the old tax system and tax exclusive line", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
oldTaxes: true,
|
||||
includes_tax: false,
|
||||
})
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/orders/${order.id}/return`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has default tax rate 12.5 therefore refund amount should be
|
||||
* 1000 * 1.125 = 1125
|
||||
*/
|
||||
expect(response.data.order.returns[0].refund_amount).toEqual(1125)
|
||||
expect(response.data.order.returns[0].items).toHaveLength(1)
|
||||
expect(response.data.order.returns[0].items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a return with tax inclusive line", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
includes_tax: true,
|
||||
})
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/orders/${order.id}/return`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has a tax rate of 20% therefore refund amount should be
|
||||
* 1000 * 1.2 = 1200
|
||||
*/
|
||||
expect(response.data.order.returns[0].refund_amount).toEqual(1200)
|
||||
expect(response.data.order.returns[0].items).toHaveLength(1)
|
||||
expect(response.data.order.returns[0].items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a return with tax exclusive line", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
includes_tax: false,
|
||||
})
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/orders/${order.id}/return`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has a tax rate of 20% therefore refund amount should be
|
||||
* 1000 * 1.2 = 1200
|
||||
*/
|
||||
expect(response.data.order.returns[0].refund_amount).toEqual(1200)
|
||||
expect(response.data.order.returns[0].items).toHaveLength(1)
|
||||
expect(response.data.order.returns[0].items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
+12
-12
@@ -9,29 +9,29 @@ const {
|
||||
MoneyAmount,
|
||||
} = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const setupServer = require("../../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const cartSeeder = require("../../helpers/cart-seeder")
|
||||
const productSeeder = require("../../helpers/product-seeder")
|
||||
const swapSeeder = require("../../helpers/swap-seeder")
|
||||
const cartSeeder = require("../../../helpers/cart-seeder")
|
||||
const productSeeder = require("../../../helpers/product-seeder")
|
||||
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||
const {
|
||||
simpleCartFactory,
|
||||
simpleRegionFactory,
|
||||
simpleProductFactory,
|
||||
simpleShippingOptionFactory,
|
||||
simpleLineItemFactory,
|
||||
} = require("../../factories")
|
||||
} = require("../../../factories")
|
||||
const {
|
||||
simpleDiscountFactory,
|
||||
} = require("../../factories/simple-discount-factory")
|
||||
} = require("../../../factories/simple-discount-factory")
|
||||
const {
|
||||
simpleCustomerFactory,
|
||||
} = require("../../factories/simple-customer-factory")
|
||||
} = require("../../../factories/simple-customer-factory")
|
||||
const {
|
||||
simpleCustomerGroupFactory,
|
||||
} = require("../../factories/simple-customer-group-factory")
|
||||
} = require("../../../factories/simple-customer-group-factory")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("/store/carts", () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd, verbose: false })
|
||||
})
|
||||
@@ -1813,7 +1813,7 @@ describe("/store/carts", () => {
|
||||
type: "swap",
|
||||
})
|
||||
|
||||
const cartWithCustomSo = await manager.save(_cart)
|
||||
await manager.save(_cart)
|
||||
|
||||
await manager.insert(CustomShippingOption, {
|
||||
id: "another-cso-test",
|
||||
@@ -0,0 +1,538 @@
|
||||
const path = require("path")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const {
|
||||
simpleCartFactory,
|
||||
simpleRegionFactory,
|
||||
simpleShippingOptionFactory,
|
||||
simpleCustomShippingOptionFactory,
|
||||
simpleProductFactory,
|
||||
simplePriceListFactory,
|
||||
simpleDiscountFactory,
|
||||
} = require("../../../factories")
|
||||
const { IdMap } = require("medusa-test-utils")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /store/carts/:id/shipping-methods", () => {
|
||||
let includesTaxShippingOption
|
||||
let cart
|
||||
let customSoCart
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
const shippingAddress = {
|
||||
id: "test-shipping-address",
|
||||
first_name: "lebron",
|
||||
country_code: "us",
|
||||
}
|
||||
const region = await simpleRegionFactory(dbConnection, {
|
||||
id: "test-region",
|
||||
})
|
||||
cart = await simpleCartFactory(dbConnection, {
|
||||
id: "test-cart",
|
||||
email: "some-customer1@email.com",
|
||||
region: region.id,
|
||||
shipping_address: shippingAddress,
|
||||
currency_code: "usd",
|
||||
})
|
||||
customSoCart = await simpleCartFactory(dbConnection, {
|
||||
id: "test-cart-with-cso",
|
||||
email: "some-customer2@email.com",
|
||||
region: region.id,
|
||||
shipping_address: shippingAddress,
|
||||
currency_code: "usd",
|
||||
})
|
||||
includesTaxShippingOption = await simpleShippingOptionFactory(
|
||||
dbConnection,
|
||||
{
|
||||
includes_tax: true,
|
||||
region_id: region.id,
|
||||
}
|
||||
)
|
||||
await simpleCustomShippingOptionFactory(dbConnection, {
|
||||
id: "another-cso-test",
|
||||
cart_id: customSoCart.id,
|
||||
shipping_option_id: includesTaxShippingOption.id,
|
||||
price: 5,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("should add a normal shipping method to the cart", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const cartWithShippingMethodRes = await api.post(
|
||||
`/store/carts/${cart.id}/shipping-methods`,
|
||||
{
|
||||
option_id: includesTaxShippingOption.id,
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
|
||||
expect(cartWithShippingMethodRes.status).toEqual(200)
|
||||
expect(cartWithShippingMethodRes.data.cart.shipping_methods).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
shipping_option_id: includesTaxShippingOption.id,
|
||||
includes_tax: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should add a custom shipping method to the cart", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const cartWithCustomShippingMethodRes = await api
|
||||
.post(
|
||||
`/store/carts/${customSoCart.id}/shipping-methods`,
|
||||
{
|
||||
option_id: includesTaxShippingOption.id,
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.catch((err) => err.response)
|
||||
|
||||
expect(cartWithCustomShippingMethodRes.status).toEqual(200)
|
||||
expect(
|
||||
cartWithCustomShippingMethodRes.data.cart.shipping_methods
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
shipping_option_id: includesTaxShippingOption.id,
|
||||
includes_tax: true,
|
||||
price: 5,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /store/carts/:id", () => {
|
||||
const variantId1 = IdMap.getId("test-variant-1")
|
||||
const variantId2 = IdMap.getId("test-variant-2")
|
||||
const productId1 = IdMap.getId("test-product-1")
|
||||
const productId2 = IdMap.getId("test-product-2")
|
||||
const regionId = IdMap.getId("test-region")
|
||||
const regionData = {
|
||||
id: regionId,
|
||||
includes_tax: false,
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
tax_rate: 20,
|
||||
name: "region test",
|
||||
}
|
||||
const buildProductData = (productId, variantId) => {
|
||||
return {
|
||||
id: productId,
|
||||
variants: [
|
||||
{
|
||||
id: variantId,
|
||||
prices: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
const buildPriceListData = (variantId, price, includesTax) => {
|
||||
return {
|
||||
status: "active",
|
||||
type: "sale",
|
||||
prices: [
|
||||
{
|
||||
variant_id: variantId,
|
||||
amount: price,
|
||||
currency_code: "usd",
|
||||
region_id: regionId,
|
||||
},
|
||||
],
|
||||
includes_tax: includesTax,
|
||||
}
|
||||
}
|
||||
const customnerPayload = {
|
||||
email: "adrien@test.dk",
|
||||
password: "adrientest",
|
||||
first_name: "adrien",
|
||||
last_name: "adrien",
|
||||
}
|
||||
const createCartPayload = {
|
||||
region_id: regionId,
|
||||
items: [
|
||||
{
|
||||
variant_id: variantId1,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
variant_id: variantId2,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe("with a cart with full tax exclusive variant pricing", () => {
|
||||
beforeEach(async () => {
|
||||
await simpleRegionFactory(dbConnection, regionData)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId1, variantId1)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId1, 100, false)
|
||||
)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId2, variantId2)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId2, 100, false)
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("should calculates correct payment totals on cart completion", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const customerRes = await api.post(
|
||||
"/store/customers",
|
||||
customnerPayload,
|
||||
{ withCredentials: true }
|
||||
)
|
||||
|
||||
const createCartRes = await api.post("/store/carts", createCartPayload)
|
||||
|
||||
const cart = createCartRes.data.cart
|
||||
|
||||
await api.post(`/store/carts/${cart.id}`, {
|
||||
customer_id: customerRes.data.customer.id,
|
||||
})
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||
|
||||
const createdOrder = await api.post(
|
||||
`/store/carts/${cart.id}/complete-cart`
|
||||
)
|
||||
|
||||
expect(createdOrder.data.type).toEqual("order")
|
||||
expect(createdOrder.data.data.discount_total).toEqual(0)
|
||||
expect(createdOrder.data.data.subtotal).toEqual(200)
|
||||
expect(createdOrder.data.data.total).toEqual(240)
|
||||
|
||||
expect(createdOrder.status).toEqual(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe("with a cart with full tax inclusive variant pricing", () => {
|
||||
beforeEach(async () => {
|
||||
await simpleRegionFactory(dbConnection, regionData)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId1, variantId1)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId1, 120, true)
|
||||
)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId2, variantId2)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId2, 120, true)
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("should calculates correct payment totals on cart completion", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const customerRes = await api.post(
|
||||
"/store/customers",
|
||||
customnerPayload,
|
||||
{ withCredentials: true }
|
||||
)
|
||||
|
||||
const createCartRes = await api.post("/store/carts", createCartPayload)
|
||||
|
||||
const cart = createCartRes.data.cart
|
||||
|
||||
await api.post(`/store/carts/${cart.id}`, {
|
||||
customer_id: customerRes.data.customer.id,
|
||||
})
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||
|
||||
const createdOrder = await api.post(
|
||||
`/store/carts/${cart.id}/complete-cart`
|
||||
)
|
||||
|
||||
expect(createdOrder.data.type).toEqual("order")
|
||||
expect(createdOrder.data.data.discount_total).toEqual(0)
|
||||
expect(createdOrder.data.data.subtotal).toEqual(200)
|
||||
expect(createdOrder.data.data.total).toEqual(240)
|
||||
|
||||
expect(createdOrder.status).toEqual(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe("with a cart mixing tax inclusive and exclusive variant pricing", () => {
|
||||
beforeEach(async () => {
|
||||
await simpleRegionFactory(dbConnection, regionData)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId1, variantId1)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId1, 120, true)
|
||||
)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId2, variantId2)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId2, 100, false)
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("should calculates correct payment totals on cart completion", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const customerRes = await api.post(
|
||||
"/store/customers",
|
||||
customnerPayload,
|
||||
{ withCredentials: true }
|
||||
)
|
||||
|
||||
const createCartRes = await api.post("/store/carts", createCartPayload)
|
||||
|
||||
const cart = createCartRes.data.cart
|
||||
|
||||
await api.post(`/store/carts/${cart.id}`, {
|
||||
customer_id: customerRes.data.customer.id,
|
||||
})
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||
|
||||
const createdOrder = await api.post(
|
||||
`/store/carts/${cart.id}/complete-cart`
|
||||
)
|
||||
|
||||
expect(createdOrder.data.type).toEqual("order")
|
||||
expect(createdOrder.data.data.discount_total).toEqual(0)
|
||||
expect(createdOrder.data.data.subtotal).toEqual(200)
|
||||
expect(createdOrder.data.data.total).toEqual(240)
|
||||
|
||||
expect(createdOrder.status).toEqual(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /store/carts/:id/line-items", () => {
|
||||
const cartIdWithItemPercentageDiscount =
|
||||
"test-cart-w-item-percentage-discount"
|
||||
const percentage15discountId = IdMap.getId("percentage15discountId")
|
||||
const variantId1 = IdMap.getId("test-variant-1")
|
||||
const variantId2 = IdMap.getId("test-variant-2")
|
||||
const productId1 = IdMap.getId("test-product-1")
|
||||
const productId2 = IdMap.getId("test-product-2")
|
||||
const regionId = IdMap.getId("test-region")
|
||||
const regionData = {
|
||||
id: regionId,
|
||||
includes_tax: false,
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
tax_rate: 20,
|
||||
name: "region test",
|
||||
}
|
||||
const buildProductData = (productId, variantId) => {
|
||||
return {
|
||||
id: productId,
|
||||
variants: [
|
||||
{
|
||||
id: variantId,
|
||||
prices: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
const buildPriceListData = (variantId, price, includesTax) => {
|
||||
return {
|
||||
status: "active",
|
||||
type: "sale",
|
||||
prices: [
|
||||
{
|
||||
variant_id: variantId,
|
||||
amount: price,
|
||||
currency_code: "usd",
|
||||
region_id: regionId,
|
||||
},
|
||||
],
|
||||
includes_tax: includesTax,
|
||||
}
|
||||
}
|
||||
|
||||
describe("with a cart mixing tax inclusive and exclusive variant pricing", () => {
|
||||
beforeEach(async () => {
|
||||
const region = await simpleRegionFactory(dbConnection, regionData)
|
||||
await simpleCartFactory(dbConnection, {
|
||||
id: cartIdWithItemPercentageDiscount,
|
||||
region,
|
||||
})
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId1, variantId1)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId1, 120, true)
|
||||
)
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
buildProductData(productId2, variantId2)
|
||||
)
|
||||
await simplePriceListFactory(
|
||||
dbConnection,
|
||||
buildPriceListData(variantId2, 100, false)
|
||||
)
|
||||
|
||||
const tenDaysAgo = ((today) =>
|
||||
new Date(today.setDate(today.getDate() - 10)))(new Date())
|
||||
const tenDaysFromToday = ((today) =>
|
||||
new Date(today.setDate(today.getDate() + 10)))(new Date())
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: percentage15discountId,
|
||||
code: percentage15discountId,
|
||||
regions: [regionId],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "15",
|
||||
allocation: "item",
|
||||
},
|
||||
starts_at: tenDaysAgo,
|
||||
ends_at: tenDaysFromToday,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("calculates correct item totals for percentage discount with mix of tax inclusive/exclusive items", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await api.post(`/store/carts/${cartIdWithItemPercentageDiscount}`, {
|
||||
region_id: regionId,
|
||||
discounts: [{ code: percentage15discountId }],
|
||||
})
|
||||
|
||||
await api.post(
|
||||
`/store/carts/${cartIdWithItemPercentageDiscount}/line-items`,
|
||||
{
|
||||
variant_id: variantId1,
|
||||
quantity: 2,
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
const response = await api.post(
|
||||
`/store/carts/${cartIdWithItemPercentageDiscount}/line-items`,
|
||||
{
|
||||
variant_id: variantId2,
|
||||
quantity: 2,
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
|
||||
const expectedItemTotals = {
|
||||
subtotal: 200,
|
||||
gift_card_total: 0,
|
||||
discount_total: 30,
|
||||
total: 204,
|
||||
original_total: 240,
|
||||
original_tax_total: 40,
|
||||
tax_total: 34,
|
||||
}
|
||||
|
||||
const expectedAdjustment = {
|
||||
amount: 30,
|
||||
discount_id: percentage15discountId,
|
||||
description: "discount",
|
||||
}
|
||||
|
||||
expect(response.data.cart.items).toHaveLength(2)
|
||||
expect(response.data.cart.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
includes_tax: true,
|
||||
cart_id: cartIdWithItemPercentageDiscount,
|
||||
unit_price: 120,
|
||||
variant_id: variantId1,
|
||||
quantity: 2,
|
||||
adjustments: [expect.objectContaining(expectedAdjustment)],
|
||||
...expectedItemTotals,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
includes_tax: false,
|
||||
cart_id: cartIdWithItemPercentageDiscount,
|
||||
unit_price: 100,
|
||||
variant_id: variantId2,
|
||||
quantity: 2,
|
||||
adjustments: [expect.objectContaining(expectedAdjustment)],
|
||||
...expectedItemTotals,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
const path = require("path")
|
||||
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../../helpers/start-server-with-environment").default
|
||||
|
||||
const {
|
||||
simpleOrderFactory,
|
||||
simpleProductFactory,
|
||||
} = require("../../../factories")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING]: Order Taxes", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
test("calculates taxes w. tax inclusive shipping method price", async () => {
|
||||
await simpleProductFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-product",
|
||||
variants: [
|
||||
{
|
||||
id: "test-variant",
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
const order = await simpleOrderFactory(
|
||||
dbConnection,
|
||||
{
|
||||
email: "test@testson.com",
|
||||
tax_rate: null,
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: null,
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
price: 110,
|
||||
includes_tax: true,
|
||||
shipping_option: {
|
||||
region_id: "test-region",
|
||||
},
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 20,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(`/store/orders/${order.id}`)
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.order.tax_total).toEqual(210)
|
||||
expect(response.data.order.total).toEqual(1310)
|
||||
})
|
||||
})
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
const path = require("path")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const setupServer = require("../../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const {
|
||||
simpleOrderFactory,
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
simpleCartFactory,
|
||||
simpleProductFactory,
|
||||
simpleProductTaxRateFactory,
|
||||
} = require("../../factories")
|
||||
} = require("../../../factories")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("Order Taxes", () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd })
|
||||
})
|
||||
@@ -17,3 +17,4 @@ export * from "./simple-product-type-tax-rate-factory"
|
||||
export * from "./simple-price-list-factory"
|
||||
export * from "./simple-batch-job-factory"
|
||||
export * from "./simple-sales-channel-factory"
|
||||
export * from "./simple-custom-shipping-option-factory"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Connection } from "typeorm"
|
||||
import faker from "faker"
|
||||
import {
|
||||
CustomShippingOption,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export type CustomShippingOptionFactoryData = {
|
||||
id?: string
|
||||
cart_id: string
|
||||
shipping_option_id: string
|
||||
price?: number
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const simpleCustomShippingOptionFactory = async (
|
||||
connection: Connection,
|
||||
data: CustomShippingOptionFactoryData,
|
||||
seed?: number
|
||||
): Promise<CustomShippingOption> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
faker.seed(seed)
|
||||
}
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
const customShippingOptionData = {
|
||||
id: data.id ?? `custon-simple-so-${Math.random() * 1000}`,
|
||||
price: typeof data.price !== "undefined" ? data.price : 500,
|
||||
cart_id: data.cart_id,
|
||||
shipping_option_id: data.shipping_option_id,
|
||||
metadata: data.metadata ?? {}
|
||||
}
|
||||
|
||||
const created = manager.create(CustomShippingOption, customShippingOptionData)
|
||||
return await manager.save(created)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { DiscountConditionJoinTableForeignKey } from "@medusajs/medusa/dist/repo
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
|
||||
export type DiscuntConditionFactoryData = {
|
||||
export type DiscountConditionFactoryData = {
|
||||
id?: string
|
||||
rule_id: string
|
||||
type: DiscountConditionType
|
||||
@@ -67,7 +67,7 @@ const getJoinTableResourceIdentifiers = (type: string) => {
|
||||
|
||||
export const simpleDiscountConditionFactory = async (
|
||||
connection: Connection,
|
||||
data: DiscuntConditionFactoryData,
|
||||
data: DiscountConditionFactoryData,
|
||||
seed?: number
|
||||
): Promise<void> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
DiscuntConditionFactoryData,
|
||||
DiscountConditionFactoryData,
|
||||
simpleDiscountConditionFactory,
|
||||
} from "./simple-discount-condition-factory"
|
||||
|
||||
@@ -15,7 +15,7 @@ export type DiscountRuleFactoryData = {
|
||||
type?: DiscountRuleType
|
||||
value?: number
|
||||
allocation?: AllocationType
|
||||
conditions: DiscuntConditionFactoryData[]
|
||||
conditions: DiscountConditionFactoryData[]
|
||||
}
|
||||
|
||||
export type DiscountFactoryData = {
|
||||
@@ -24,6 +24,8 @@ export type DiscountFactoryData = {
|
||||
is_dynamic?: boolean
|
||||
rule?: DiscountRuleFactoryData
|
||||
regions?: string[]
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
}
|
||||
|
||||
export const simpleDiscountFactory = async (
|
||||
@@ -37,7 +39,7 @@ export const simpleDiscountFactory = async (
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
const ruleData = data.rule ?? {}
|
||||
const ruleData = data.rule ?? ({} as DiscountRuleFactoryData)
|
||||
const ruleToSave = manager.create(DiscountRule, {
|
||||
type: ruleData.type ?? DiscountRuleType.PERCENTAGE,
|
||||
value: ruleData.value ?? 10,
|
||||
@@ -63,8 +65,9 @@ export const simpleDiscountFactory = async (
|
||||
rule_id: dRule.id,
|
||||
code: data.code ?? "TESTCODE",
|
||||
regions: data.regions?.map((r) => ({ id: r })) || [],
|
||||
starts_at: data.starts_at,
|
||||
ends_at: data.ends_at,
|
||||
})
|
||||
|
||||
const discount = await manager.save(toSave)
|
||||
return discount
|
||||
return await manager.save(toSave)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type LineItemFactoryData = {
|
||||
returned_quantity?: boolean
|
||||
tax_lines?: TaxLineFactoryData[]
|
||||
adjustments: LineItemAdjustmentFactoryData[]
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export const simpleLineItemFactory = async (
|
||||
@@ -70,6 +71,7 @@ export const simpleLineItemFactory = async (
|
||||
shipped_quantity: data.shipped_quantity || null,
|
||||
returned_quantity: data.returned_quantity || null,
|
||||
adjustments: data.adjustments,
|
||||
includes_tax: data.includes_tax,
|
||||
})
|
||||
|
||||
const line = await manager.save(toSave)
|
||||
|
||||
@@ -26,6 +26,7 @@ export type PriceListFactoryData = {
|
||||
ends_at?: Date
|
||||
customer_groups?: string[]
|
||||
prices?: ProductListPrice[]
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export const simplePriceListFactory = async (
|
||||
@@ -59,6 +60,7 @@ export const simplePriceListFactory = async (
|
||||
starts_at: data.starts_at || null,
|
||||
ends_at: data.ends_at || null,
|
||||
customer_groups: customerGroups,
|
||||
includes_tax: data.includes_tax,
|
||||
}
|
||||
|
||||
const toSave = manager.create(PriceList, toCreate)
|
||||
|
||||
@@ -10,6 +10,8 @@ export type RegionFactoryData = {
|
||||
countries?: string[]
|
||||
automatic_taxes?: boolean
|
||||
gift_cards_taxable?: boolean
|
||||
fulfillment_providers?: { id: string }[]
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export const simpleRegionFactory = async (
|
||||
@@ -30,7 +32,9 @@ export const simpleRegionFactory = async (
|
||||
currency_code: data.currency_code || "usd",
|
||||
tax_rate: data.tax_rate || 0,
|
||||
payment_providers: [{ id: "test-pay" }],
|
||||
fulfillment_providers: data.fulfillment_providers ?? [{ id: "test-ful" }],
|
||||
gift_cards_taxable: data.gift_cards_taxable ?? true,
|
||||
includes_tax: data.includes_tax,
|
||||
automatic_taxes:
|
||||
typeof data.automatic_taxes !== "undefined" ? data.automatic_taxes : true,
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ShippingMethodFactoryData = {
|
||||
price?: number
|
||||
shipping_option: string | ShippingOptionFactoryData
|
||||
tax_lines?: ShippingMethodTaxLine[]
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export const simpleShippingMethodFactory = async (
|
||||
@@ -47,6 +48,7 @@ export const simpleShippingMethodFactory = async (
|
||||
shipping_option_id: shippingOptionId,
|
||||
data: data.data || {},
|
||||
price: typeof data.price !== "undefined" ? data.price : 500,
|
||||
includes_tax: data.includes_tax,
|
||||
})
|
||||
|
||||
const shippingMethod = await manager.save(toSave)
|
||||
|
||||
@@ -8,12 +8,14 @@ import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
|
||||
export type ShippingOptionFactoryData = {
|
||||
id?: string
|
||||
name?: string
|
||||
region_id: string
|
||||
is_return?: boolean
|
||||
is_giftcard?: boolean
|
||||
price?: number
|
||||
price_type?: ShippingOptionPriceType
|
||||
includes_tax?: boolean
|
||||
data?: object
|
||||
}
|
||||
|
||||
@@ -35,8 +37,8 @@ export const simpleShippingOptionFactory = async (
|
||||
type: ShippingProfileType.GIFT_CARD,
|
||||
})
|
||||
|
||||
const created = manager.create(ShippingOption, {
|
||||
id: `simple-so-${Math.random() * 1000}`,
|
||||
const shippingOptionData = {
|
||||
id: data.id ?? `simple-so-${Math.random() * 1000}`,
|
||||
name: data.name || "Test Method",
|
||||
is_return: data.is_return ?? false,
|
||||
region_id: data.region_id,
|
||||
@@ -45,7 +47,15 @@ export const simpleShippingOptionFactory = async (
|
||||
price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
|
||||
data: data.data ?? {},
|
||||
amount: typeof data.price !== "undefined" ? data.price : 500,
|
||||
})
|
||||
const option = await manager.save(created)
|
||||
return option
|
||||
}
|
||||
|
||||
// This is purposefully managed out of the original object for the purpose of separating the data linked to a feature flag
|
||||
// MEDUSA_FF_TAX_INCLUSIVE_PRICING
|
||||
const { includes_tax } = data
|
||||
if (typeof includes_tax !== "undefined") {
|
||||
shippingOptionData["includes_tax"] = includes_tax
|
||||
}
|
||||
|
||||
const created = manager.create(ShippingOption, shippingOptionData)
|
||||
return await manager.save(created)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { currency, CurrencyServiceMock } from "../../../../../services/__mocks__/currency";
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../../loaders/feature-flags/tax-inclusive-pricing";
|
||||
|
||||
describe("GET /admin/currencies/", () => {
|
||||
describe("successfully list the currency", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"GET",
|
||||
`/admin/currencies`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: [TaxInclusivePricingFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls the listAndCount method from the currency service", () => {
|
||||
expect(CurrencyServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(CurrencyServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{},
|
||||
{
|
||||
order: {},
|
||||
select: undefined,
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 20
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns the expected currencies", () => {
|
||||
expect(subject.body).toEqual({
|
||||
currencies: [currency],
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
count: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { currency, CurrencyServiceMock } from "../../../../../services/__mocks__/currency";
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../../loaders/feature-flags/tax-inclusive-pricing";
|
||||
|
||||
describe("POST /admin/currencies/:code", () => {
|
||||
let subject
|
||||
const code = IdMap.getId("currency-1")
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/currencies/${code}`,
|
||||
{
|
||||
payload: {
|
||||
includes_tax: true,
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: [TaxInclusivePricingFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns updated currency", () => {
|
||||
expect(subject.body.currency).toEqual({
|
||||
...currency,
|
||||
includes_tax: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls service update", () => {
|
||||
expect(CurrencyServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(CurrencyServiceMock.update).toHaveBeenCalledWith(
|
||||
code,
|
||||
{
|
||||
includes_tax: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Router } from "express"
|
||||
import middlewares, {
|
||||
transformBody,
|
||||
transformQuery,
|
||||
} from "../../../middlewares"
|
||||
import { AdminGetCurrenciesParams } from "./list-currencies"
|
||||
import { AdminPostCurrenciesCurrencyReq } from "./update-currency"
|
||||
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
export default (app) => {
|
||||
const route = Router()
|
||||
app.use(
|
||||
"/currencies",
|
||||
isFeatureFlagEnabled(TaxInclusivePricingFeatureFlag.key),
|
||||
route
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/",
|
||||
transformQuery(AdminGetCurrenciesParams, {
|
||||
isList: true,
|
||||
}),
|
||||
middlewares.wrap(require("./list-currencies").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:code",
|
||||
transformBody(AdminPostCurrenciesCurrencyReq),
|
||||
middlewares.wrap(require("./update-currency").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export * from "./list-currencies"
|
||||
export * from "./update-currency"
|
||||
@@ -0,0 +1,76 @@
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator"
|
||||
import { Currency } from "../../../../models"
|
||||
import { CurrencyService } from "../../../../services"
|
||||
import { ExtendedRequest } from "../../../../types/global"
|
||||
import { FindConfig, FindPaginationParams } from "../../../../types/common"
|
||||
|
||||
/**
|
||||
* @oas [get] /currencies
|
||||
* operationId: "GetCurrencies"
|
||||
* summary: "List Currency"
|
||||
* description: "Retrieves a list of Currency"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) code {string} Code of the currency to search for.
|
||||
* - (query) includes_tax {boolean} Search for tax inclusive currencies.
|
||||
* - (query) order {string} to retrieve products in.
|
||||
* - (query) offset {string} How many products to skip in the result.
|
||||
* - (query) limit {string} Limit the number of products returned.
|
||||
* tags:
|
||||
* - Currency
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* count:
|
||||
* description: The number of Currency.
|
||||
* type: integer
|
||||
* offset:
|
||||
* description: The offset of the Currency query.
|
||||
* type: integer
|
||||
* limit:
|
||||
* description: The limit of the currency query.
|
||||
* type: integer
|
||||
* currencies:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/currency"
|
||||
*/
|
||||
export default async (req: ExtendedRequest<Currency>, res) => {
|
||||
const currencyService: CurrencyService = req.scope.resolve("currencyService")
|
||||
|
||||
const { skip, take } = req.listConfig
|
||||
|
||||
req.listConfig.select = undefined
|
||||
if (req.listConfig.order && req.listConfig.order["created_at"]) {
|
||||
delete req.listConfig.order["created_at"]
|
||||
}
|
||||
const [currencies, count] = await currencyService.listAndCount(
|
||||
req.filterableFields,
|
||||
req.listConfig
|
||||
)
|
||||
|
||||
res.json({
|
||||
currencies,
|
||||
count,
|
||||
offset: skip,
|
||||
limit: take,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetCurrenciesParams extends FindPaginationParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
code?: string
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
includes_tax?: boolean
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IsBoolean, IsOptional } from "class-validator"
|
||||
import { Currency } from "../../../../models"
|
||||
import { ExtendedRequest } from "../../../../types/global"
|
||||
import { CurrencyService } from "../../../../services"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
/**
|
||||
* @oas [post] /currencies/:code
|
||||
* operationId: "PostCurrenciesCurrency"
|
||||
* summary: "Update a Currency"
|
||||
* description: "Update a Currency"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) code=* {string} The code of the Currency.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* includes_tax:
|
||||
* type: boolean
|
||||
* description: [EXPERIMENTAL] Tax included in prices of currency.
|
||||
* tags:
|
||||
* - Currency
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* currency:
|
||||
* $ref: "#/components/schemas/currency"
|
||||
*/
|
||||
export default async (req: ExtendedRequest<Currency>, res) => {
|
||||
const code = req.params.code as string
|
||||
const data = req.validatedBody as AdminPostCurrenciesCurrencyReq
|
||||
const currencyService: CurrencyService = req.scope.resolve("currencyService")
|
||||
|
||||
const currency = await currencyService.update(code, data)
|
||||
|
||||
res.json({ currency })
|
||||
}
|
||||
|
||||
export class AdminPostCurrenciesCurrencyReq {
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { Router } from "express"
|
||||
import middlewares from "../../middlewares"
|
||||
import appRoutes from "./apps"
|
||||
import authRoutes from "./auth"
|
||||
import batchRoutes from "./batch"
|
||||
import collectionRoutes from "./collections"
|
||||
import currencyRoutes from "./currencies"
|
||||
import customerGroupRoutes from "./customer-groups"
|
||||
import customerRoutes from "./customers"
|
||||
import discountRoutes from "./discounts"
|
||||
@@ -14,7 +16,6 @@ import noteRoutes from "./notes"
|
||||
import notificationRoutes from "./notifications"
|
||||
import orderRoutes from "./orders"
|
||||
import priceListRoutes from "./price-lists"
|
||||
import batchRoutes from "./batch"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import productRoutes from "./products"
|
||||
@@ -70,6 +71,7 @@ export default (app, container, config) => {
|
||||
collectionRoutes(route)
|
||||
customerGroupRoutes(route)
|
||||
customerRoutes(route)
|
||||
currencyRoutes(route)
|
||||
discountRoutes(route)
|
||||
draftOrderRoutes(route)
|
||||
giftCardRoutes(route)
|
||||
@@ -77,15 +79,15 @@ export default (app, container, config) => {
|
||||
noteRoutes(route)
|
||||
notificationRoutes(route)
|
||||
orderRoutes(route, featureFlagRouter)
|
||||
priceListRoutes(route)
|
||||
priceListRoutes(route, featureFlagRouter)
|
||||
productRoutes(route, featureFlagRouter)
|
||||
productTagRoutes(route)
|
||||
productTypesRoutes(route)
|
||||
regionRoutes(route)
|
||||
regionRoutes(route, featureFlagRouter)
|
||||
returnReasonRoutes(route)
|
||||
returnRoutes(route)
|
||||
salesChannelRoutes(route)
|
||||
shippingOptionRoutes(route)
|
||||
shippingOptionRoutes(route, featureFlagRouter)
|
||||
shippingProfileRoutes(route)
|
||||
storeRoutes(route)
|
||||
swapRoutes(route)
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import {
|
||||
AdminPriceListPricesCreateReq,
|
||||
CreatePriceListInput,
|
||||
PriceListStatus,
|
||||
PriceListType,
|
||||
} from "../../../../types/price-list"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { Request } from "express"
|
||||
import { Type } from "class-transformer"
|
||||
import { Request } from "express"
|
||||
import { EntityManager } from "typeorm"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
|
||||
/**
|
||||
* @oas [post] /price-lists
|
||||
@@ -98,6 +101,9 @@ import { Type } from "class-transformer"
|
||||
* id:
|
||||
* description: The ID of a customer group
|
||||
* type: string
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of price list"
|
||||
* type: boolean
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
@@ -215,4 +221,10 @@ export class AdminPostPriceListsPriceListReq {
|
||||
@Type(() => CustomerGroup)
|
||||
@ValidateNested({ each: true })
|
||||
customer_groups?: CustomerGroup[]
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
@@ -14,12 +14,18 @@ import {
|
||||
defaultAdminProductRelations,
|
||||
} from "../products"
|
||||
import { AdminPostPriceListsPriceListReq } from "./create-price-list"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
export default (app, featureFlagRouter: FlagRouter) => {
|
||||
app.use("/price-lists", route)
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)) {
|
||||
defaultAdminPriceListFields.push("includes_tax")
|
||||
}
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-price-list").default))
|
||||
|
||||
route.get(
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import {
|
||||
AdminPriceListPricesUpdateReq,
|
||||
PriceListStatus,
|
||||
PriceListType,
|
||||
} from "../../../../types/price-list"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
|
||||
import {
|
||||
AdminPriceListPricesUpdateReq,
|
||||
PriceListStatus,
|
||||
PriceListType,
|
||||
} from "../../../../types/price-list"
|
||||
|
||||
import { PriceList } from "../../../.."
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { Type } from "class-transformer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { PriceList } from "../../../.."
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /price-lists/{id}
|
||||
@@ -99,6 +102,9 @@ import { EntityManager } from "typeorm"
|
||||
* id:
|
||||
* description: The ID of a customer group
|
||||
* type: string
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of price list"
|
||||
* type: boolean
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
@@ -213,4 +219,10 @@ export class AdminPostPriceListsPriceListPriceListReq {
|
||||
@Type(() => CustomerGroup)
|
||||
@ValidateNested({ each: true })
|
||||
customer_groups?: CustomerGroup[]
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { defaultAdminRegionFields, defaultAdminRegionRelations } from "."
|
||||
import { Region } from "../../../.."
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import RegionService from "../../../../services/region"
|
||||
import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /regions
|
||||
@@ -61,6 +63,9 @@ import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of region"
|
||||
* type: boolean
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
@@ -179,6 +184,12 @@ export class AdminPostRegionsReq {
|
||||
@IsString({ each: true })
|
||||
countries: string[]
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
@@ -3,12 +3,18 @@ import { Region } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import "reflect-metadata"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
export default (app, featureFlagRouter: FlagRouter) => {
|
||||
app.use("/regions", route)
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)) {
|
||||
defaultAdminRegionFields.push("includes_tax")
|
||||
}
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-regions").default))
|
||||
route.get("/:region_id", middlewares.wrap(require("./get-region").default))
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { defaultAdminRegionFields, defaultAdminRegionRelations } from "."
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import RegionService from "../../../../services/region"
|
||||
import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /regions/{id}
|
||||
@@ -49,6 +51,9 @@ import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
||||
* tax_rate:
|
||||
* description: "The tax rate to use on Orders in the Region."
|
||||
* type: number
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of region"
|
||||
* type: boolean
|
||||
* payment_providers:
|
||||
* description: "A list of Payment Provider IDs that should be enabled for the Region"
|
||||
* type: array
|
||||
@@ -178,6 +183,12 @@ export class AdminPostRegionsRegionReq {
|
||||
@IsOptional()
|
||||
countries?: string[]
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("POST /admin/sales-channels/:id/products/batch", () => {
|
||||
describe("add product to a sales channel", () => {
|
||||
@@ -21,7 +22,7 @@ describe("POST /admin/sales-channels/:id/products/batch", () => {
|
||||
payload: {
|
||||
product_ids: [{ id: "sales_channel_1_product_1" }],
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { IdMap } from "medusa-test-utils"
|
||||
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("POST /admin/sales-channels", () => {
|
||||
describe("successfully get a sales channel", () => {
|
||||
@@ -18,7 +19,7 @@ describe("POST /admin/sales-channels", () => {
|
||||
name: "sales channel 1 name",
|
||||
description: "sales channel 1 description",
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
||||
describe("remove product from a sales channel", () => {
|
||||
@@ -19,7 +20,7 @@ describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
||||
payload: {
|
||||
product_ids: [{ id: IdMap.getId("sales_channel_1_product_1") }]
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("DELETE /admin/sales-channels/:id", () => {
|
||||
describe("successfully delete a sales channel", () => {
|
||||
@@ -16,7 +17,7 @@ describe("DELETE /admin/sales-channels/:id", () => {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("GET /admin/sales-channels/:id", () => {
|
||||
describe("successfully get a sales channel", () => {
|
||||
@@ -16,7 +17,7 @@ describe("GET /admin/sales-channels/:id", () => {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("GET /admin/sales-channels/", () => {
|
||||
describe("successfully list the sales channel", () => {
|
||||
@@ -16,7 +17,7 @@ describe("GET /admin/sales-channels/", () => {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
describe("POST /admin/regions/:region_id/countries", () => {
|
||||
describe("successful creation", () => {
|
||||
@@ -18,7 +19,7 @@ describe("POST /admin/regions/:region_id/countries", () => {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
flags: [SalesChannelFeatureFlag],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
import { defaultFields, defaultRelations } from "."
|
||||
|
||||
import { Type } from "class-transformer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /shipping-options
|
||||
@@ -81,6 +84,9 @@ import { EntityManager } from "typeorm"
|
||||
* metadata:
|
||||
* description: An optional set of key-value pairs with additional information.
|
||||
* type: object
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of shipping option"
|
||||
* type: boolean
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
@@ -214,4 +220,10 @@ export class AdminPostShippingOptionsReq {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@ import { Router } from "express"
|
||||
import { ShippingOption } from "../../../.."
|
||||
import { PaginatedResponse, DeleteResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
export default (app, featureFlagRouter: FlagRouter) => {
|
||||
app.use("/shipping-options", route)
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)) {
|
||||
defaultFields.push("includes_tax")
|
||||
}
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-shipping-options").default))
|
||||
route.post("/", middlewares.wrap(require("./create-shipping-option").default))
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
import { defaultFields, defaultRelations } from "."
|
||||
|
||||
import { Type } from "class-transformer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /shipping-options/{id}
|
||||
@@ -60,6 +62,9 @@ import { EntityManager } from "typeorm"
|
||||
* amount:
|
||||
* description: The amount to compare with.
|
||||
* type: integer
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Tax included in prices of shipping option"
|
||||
* type: boolean
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
@@ -174,4 +179,10 @@ export class AdminPostShippingOptionsOptionReq {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
|
||||
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||
IsOptional(),
|
||||
IsBoolean(),
|
||||
])
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import supertest from "supertest"
|
||||
import querystring from "querystring"
|
||||
import apiLoader from "../loaders/api"
|
||||
import passportLoader from "../loaders/passport"
|
||||
import featureFlagLoader from "../loaders/feature-flags"
|
||||
import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
|
||||
import servicesLoader from "../loaders/services"
|
||||
import strategiesLoader from "../loaders/strategies"
|
||||
import logger from "../loaders/logger";
|
||||
|
||||
const adminSessionOpts = {
|
||||
cookieName: "session",
|
||||
@@ -36,8 +37,6 @@ const testApp = express()
|
||||
|
||||
const container = createContainer()
|
||||
|
||||
const featureFlagRouter = featureFlagLoader(config)
|
||||
|
||||
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
||||
container.register("configModule", asValue(config))
|
||||
container.register({
|
||||
@@ -60,6 +59,7 @@ testApp.use((req, res, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
featureFlagLoader(config)
|
||||
servicesLoader({ container, configModule: config })
|
||||
strategiesLoader({ container, configModule: config })
|
||||
passportLoader({ app: testApp, container, configModule: config })
|
||||
@@ -77,7 +77,7 @@ export async function request(method, url, opts = {}) {
|
||||
const { payload, query, headers = {}, flags = [] } = opts
|
||||
|
||||
flags.forEach((flag) => {
|
||||
featureFlagRouter.setFlag(flag, true)
|
||||
featureFlagRouter.setFlag(flag.key, true)
|
||||
})
|
||||
|
||||
const queryParams = query && querystring.stringify(query)
|
||||
@@ -148,6 +148,5 @@ export async function request(method, url, opts = {}) {
|
||||
// c[clientSessionOpts.cookieName] &&
|
||||
// sessions.util.decode(clientSessionOpts, c[clientSessionOpts.cookieName])
|
||||
// .content
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MoneyAmount } from ".."
|
||||
import { PriceListType } from "../types/price-list"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
|
||||
export interface IPriceSelectionStrategy {
|
||||
/**
|
||||
@@ -55,6 +56,7 @@ export type PriceSelectionContext = {
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
include_discount_prices?: boolean
|
||||
tax_rates?: TaxServiceRate[]
|
||||
}
|
||||
|
||||
enum DefaultPriceType {
|
||||
@@ -67,7 +69,9 @@ export const PriceType = { ...DefaultPriceType, ...PriceListType }
|
||||
|
||||
export type PriceSelectionResult = {
|
||||
originalPrice: number | null
|
||||
originalPriceIncludesTax?: boolean | null
|
||||
calculatedPrice: number | null
|
||||
calculatedPriceIncludesTax?: boolean | null
|
||||
calculatedPriceType?: PriceType
|
||||
prices: MoneyAmount[] // prices is an array of all possible price for the input customer and region prices
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const isTruthy = (val: string | boolean | undefined): boolean => {
|
||||
return !!val
|
||||
}
|
||||
|
||||
export const featureFlagRouter = new FlagRouter({})
|
||||
|
||||
export default (
|
||||
configModule: { featureFlags?: Record<string, string | boolean> } = {},
|
||||
logger?: Logger,
|
||||
@@ -60,5 +62,9 @@ export default (
|
||||
}
|
||||
}
|
||||
|
||||
return new FlagRouter(flagConfig)
|
||||
for (const flag of Object.keys(flagConfig)) {
|
||||
featureFlagRouter.setFlag(flag, flagConfig[flag])
|
||||
}
|
||||
|
||||
return featureFlagRouter
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
|
||||
const TaxInclusivePricingFeatureFlag: FlagSettings = {
|
||||
key: "tax_inclusive_pricing",
|
||||
default_val: false,
|
||||
env_key: "MEDUSA_FF_TAX_INCLUSIVE_PRICING",
|
||||
description: "[WIP] Enable tax inclusive pricing",
|
||||
}
|
||||
|
||||
export default TaxInclusivePricingFeatureFlag
|
||||
@@ -0,0 +1,46 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
import TaxInclusivePricingFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
export const featureFlag = TaxInclusivePricingFlag.key
|
||||
|
||||
export class test1659501357661 implements MigrationInterface {
|
||||
name = "test1659501357661"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "currency" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_option" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "price_list" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" ADD "includes_tax" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP COLUMN "includes_tax"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" DROP COLUMN "includes_tax"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "price_list" DROP COLUMN "includes_tax"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_option" DROP COLUMN "includes_tax"`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "includes_tax"`)
|
||||
await queryRunner.query(`ALTER TABLE "currency" DROP COLUMN "includes_tax"`)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Column, Entity, PrimaryColumn } from "typeorm"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
@Entity()
|
||||
export class Currency {
|
||||
@@ -13,6 +15,9 @@ export class Currency {
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,4 +50,7 @@ export class Currency {
|
||||
* description: "The written name of the currency"
|
||||
* type: string
|
||||
* example: US Dollar
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Does the currency prices include tax"
|
||||
* type: boolean
|
||||
*/
|
||||
|
||||
@@ -92,4 +92,7 @@ export class CustomShippingOption extends SoftDeletableEntity {
|
||||
* type: object
|
||||
* description: An optional key-value map with additional details
|
||||
* example: {car: "white"}
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Indicates if the custom shipping option price include tax"
|
||||
* type: boolean
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@ import { Order } from "./order"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { Swap } from "./swap"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
@Check(`"fulfilled_quantity" <= "quantity"`)
|
||||
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
|
||||
@@ -116,6 +118,9 @@ export class LineItem extends BaseEntity {
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown>
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax: boolean
|
||||
|
||||
refundable?: number | null
|
||||
subtotal?: number | null
|
||||
tax_total?: number | null
|
||||
@@ -275,6 +280,9 @@ export class LineItem extends BaseEntity {
|
||||
* type: integer
|
||||
* description: The total of the gift card of the line item
|
||||
* example: 0
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Indicates if the line item unit_price include tax"
|
||||
* type: boolean
|
||||
* created_at:
|
||||
* type: string
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
|
||||
@@ -21,7 +21,7 @@ export class MoneyAmount extends SoftDeletableEntity {
|
||||
|
||||
@ManyToOne(() => Currency)
|
||||
@JoinColumn({ name: "currency_code", referencedColumnName: "code" })
|
||||
currency: Currency
|
||||
currency?: Currency
|
||||
|
||||
@Column({ type: "int" })
|
||||
amount: number
|
||||
@@ -58,7 +58,7 @@ export class MoneyAmount extends SoftDeletableEntity {
|
||||
|
||||
@ManyToOne(() => Region)
|
||||
@JoinColumn({ name: "region_id" })
|
||||
region: Region
|
||||
region?: Region
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): undefined | void {
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CustomerGroup } from "./customer-group"
|
||||
import { MoneyAmount } from "./money-amount"
|
||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
@Entity()
|
||||
export class PriceList extends SoftDeletableEntity {
|
||||
@@ -58,6 +60,9 @@ export class PriceList extends SoftDeletableEntity {
|
||||
})
|
||||
prices: MoneyAmount[]
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax: boolean
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): undefined | void {
|
||||
this.id = generateEntityId(this.id, "pl")
|
||||
@@ -118,6 +123,10 @@ export class PriceList extends SoftDeletableEntity {
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/money_amount"
|
||||
* $ref: "#/components/schemas/customer_group"
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Does the price list prices include tax"
|
||||
* type: boolean
|
||||
* created_at:
|
||||
* type: string
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
|
||||
@@ -18,6 +18,8 @@ import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||
import { TaxProvider } from "./tax-provider"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
@Entity()
|
||||
export class Region extends SoftDeletableEntity {
|
||||
@@ -93,6 +95,9 @@ export class Region extends SoftDeletableEntity {
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown>
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax: boolean
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "reg")
|
||||
@@ -170,6 +175,9 @@ export class Region extends SoftDeletableEntity {
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/fulfillment_provider"
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Does the prices for the region include tax"
|
||||
* type: boolean
|
||||
* created_at:
|
||||
* type: string
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
|
||||
@@ -20,6 +20,8 @@ import { ShippingMethodTaxLine } from "./shipping-method-tax-line"
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
import { Swap } from "./swap"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
@Check(
|
||||
`"claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
|
||||
@@ -89,6 +91,9 @@ export class ShippingMethod {
|
||||
@DbAwareColumn({ type: "jsonb" })
|
||||
data: Record<string, unknown>
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax: boolean
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "sm")
|
||||
@@ -163,4 +168,7 @@ export class ShippingMethod {
|
||||
* description: "Additional data that the Fulfillment Provider needs to fulfill the shipment. This is used in combination with the Shipping Options data, and may contain information such as a drop point id."
|
||||
* type: object
|
||||
* example: {}
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Indicates if the shipping method price include tax"
|
||||
* type: boolean
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,8 @@ import { ShippingOptionRequirement } from "./shipping-option-requirement"
|
||||
import { ShippingProfile } from "./shipping-profile"
|
||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
export enum ShippingOptionPriceType {
|
||||
FLAT_RATE = "flat_rate",
|
||||
@@ -75,6 +77,9 @@ export class ShippingOption extends SoftDeletableEntity {
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown>
|
||||
|
||||
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||
includes_tax: boolean
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "so")
|
||||
@@ -146,6 +151,9 @@ export class ShippingOption extends SoftDeletableEntity {
|
||||
* description: "The data needed for the Fulfillment Provider to identify the Shipping Option."
|
||||
* type: object
|
||||
* example: {}
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Does the shipping option price include tax"
|
||||
* type: boolean
|
||||
* created_at:
|
||||
* type: string
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { Currency } from "../models/currency"
|
||||
import { Currency } from "../models"
|
||||
|
||||
@EntityRepository(Currency)
|
||||
export class CurrencyRepository extends Repository<Currency> { }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import partition from "lodash/partition"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import {
|
||||
Brackets,
|
||||
EntityRepository,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
WhereExpressionBuilder,
|
||||
} from "typeorm"
|
||||
import { MoneyAmount } from "../models/money-amount"
|
||||
import {
|
||||
@@ -126,7 +126,7 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
.leftJoinAndSelect("ma.price_list", "price_list")
|
||||
.where("ma.variant_id = :variant_id", { variant_id })
|
||||
|
||||
const getAndWhere = (subQb) => {
|
||||
const getAndWhere = (subQb): WhereExpressionBuilder => {
|
||||
const andWhere = subQb.where("ma.price_list_id = :price_list_id", {
|
||||
price_list_id,
|
||||
})
|
||||
@@ -146,7 +146,8 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
region_id?: string,
|
||||
currency_code?: string,
|
||||
customer_id?: string,
|
||||
include_discount_prices?: boolean
|
||||
include_discount_prices?: boolean,
|
||||
include_tax_inclusive_pricing = false
|
||||
): Promise<[MoneyAmount[], number]> {
|
||||
const date = new Date()
|
||||
|
||||
@@ -154,12 +155,9 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
.leftJoinAndSelect("ma.price_list", "price_list")
|
||||
.where({ variant_id: variant_id })
|
||||
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
||||
.andWhere(
|
||||
"(price_list.ends_at is null OR price_list.ends_at > :date)",
|
||||
{
|
||||
date: date.toUTCString(),
|
||||
}
|
||||
)
|
||||
.andWhere("(price_list.ends_at is null OR price_list.ends_at > :date)", {
|
||||
date: date.toUTCString(),
|
||||
})
|
||||
.andWhere(
|
||||
"(price_list.starts_at is null OR price_list.starts_at < :date)",
|
||||
{
|
||||
@@ -167,6 +165,11 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
}
|
||||
)
|
||||
|
||||
if (include_tax_inclusive_pricing) {
|
||||
qb.leftJoin("ma.currency", "currency")
|
||||
.leftJoin("ma.region", "region")
|
||||
.addSelect(["currency.includes_tax", "region.includes_tax"])
|
||||
}
|
||||
if (region_id || currency_code) {
|
||||
qb.andWhere(
|
||||
new Brackets((qb) =>
|
||||
@@ -181,14 +184,21 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
|
||||
if (customer_id) {
|
||||
qb.leftJoin("price_list.customer_groups", "cgroup")
|
||||
.leftJoin("customer_group_customers", "cgc", "cgc.customer_group_id = cgroup.id")
|
||||
.andWhere("(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)", {
|
||||
customer_id,
|
||||
})
|
||||
.leftJoin(
|
||||
"customer_group_customers",
|
||||
"cgc",
|
||||
"cgc.customer_group_id = cgroup.id"
|
||||
)
|
||||
.andWhere(
|
||||
"(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)",
|
||||
{
|
||||
customer_id,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
qb
|
||||
.leftJoin("price_list.customer_groups", "cgroup")
|
||||
.andWhere("cgroup.id is null")
|
||||
qb.leftJoin("price_list.customer_groups", "cgroup").andWhere(
|
||||
"cgroup.id is null"
|
||||
)
|
||||
}
|
||||
return await qb.getManyAndCount()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,22 @@ export const carts = {
|
||||
total: 1000,
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
},
|
||||
testCartTaxInclusive: {
|
||||
id: IdMap.getId("test-cart"),
|
||||
items: [],
|
||||
payment: {
|
||||
data: "some-data",
|
||||
},
|
||||
payment_session: {
|
||||
status: "authorized",
|
||||
},
|
||||
total: 1000,
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
shipping_options: [{
|
||||
id: IdMap.getId("tax-inclusive-option"),
|
||||
includes_tax: true
|
||||
}],
|
||||
},
|
||||
testSwapCart: {
|
||||
id: IdMap.getId("test-swap"),
|
||||
items: [],
|
||||
@@ -263,6 +279,9 @@ export const CartServiceMock = {
|
||||
if (cartId === IdMap.getId("test-cart2")) {
|
||||
return Promise.resolve(carts.testCart)
|
||||
}
|
||||
if (cartId === IdMap.getId("tax-inclusive-option")) {
|
||||
return Promise.resolve(carts.testCartTaxInclusive)
|
||||
}
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "cart not found")
|
||||
}),
|
||||
addLineItem: jest.fn().mockImplementation((cartId, lineItem) => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const currency = {
|
||||
code: IdMap.getId("currency-1"),
|
||||
symbol: "SYM",
|
||||
symbol_native: "SYM",
|
||||
name: "Symbol",
|
||||
}
|
||||
|
||||
export const CurrencyServiceMock = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockImplementation((code) => {
|
||||
return Promise.resolve({
|
||||
...currency,
|
||||
code: code,
|
||||
})
|
||||
}),
|
||||
|
||||
update: jest.fn().mockImplementation((code, data) => {
|
||||
return Promise.resolve({
|
||||
...currency,
|
||||
...data,
|
||||
})
|
||||
}),
|
||||
|
||||
listAndCount: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve([
|
||||
[currency],
|
||||
1,
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return CurrencyServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -0,0 +1,60 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { EventBusService } from "../index"
|
||||
import { Currency } from "../../models"
|
||||
import CurrencyService from "../currency"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
const currencyCode = IdMap.getId("currency-1")
|
||||
const eventBusServiceMock = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
} as unknown as EventBusService
|
||||
const currencyRepositoryMock = MockRepository({
|
||||
findOne: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
code: currencyCode
|
||||
}
|
||||
}),
|
||||
save: jest.fn().mockImplementation((data) => {
|
||||
return Object.assign(new Currency(), data)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('CurrencyService', () => {
|
||||
const currencyService = new CurrencyService({
|
||||
manager: MockManager,
|
||||
currencyRepository: currencyRepositoryMock,
|
||||
eventBusService: eventBusServiceMock,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: true
|
||||
}),
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should retrieve the currency by calling the repository findOne method", async () => {
|
||||
await currencyService.retrieveByCode(currencyCode)
|
||||
expect(currencyRepositoryMock.findOne).toHaveBeenCalledWith({
|
||||
where: { code: currencyCode.toLowerCase() },
|
||||
})
|
||||
})
|
||||
|
||||
it("should update the currency by calling the save method", async () => {
|
||||
await currencyService.update(currencyCode, {
|
||||
includes_tax: true,
|
||||
})
|
||||
expect(currencyRepositoryMock.findOne).toHaveBeenCalledWith({
|
||||
where: { code: currencyCode.toLowerCase() },
|
||||
})
|
||||
expect(currencyRepositoryMock.save).toHaveBeenCalledWith({
|
||||
code: currencyCode,
|
||||
includes_tax: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,8 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import DiscountService from "../discount"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
|
||||
const featureFlagRouter = new FlagRouter({})
|
||||
|
||||
describe("DiscountService", () => {
|
||||
describe("create", () => {
|
||||
@@ -23,6 +26,7 @@ describe("DiscountService", () => {
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
regionService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -160,6 +164,7 @@ describe("DiscountService", () => {
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -203,6 +208,7 @@ describe("DiscountService", () => {
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -248,6 +254,7 @@ describe("DiscountService", () => {
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
regionService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -345,6 +352,7 @@ describe("DiscountService", () => {
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
regionService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -418,6 +426,7 @@ describe("DiscountService", () => {
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
regionService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -466,6 +475,7 @@ describe("DiscountService", () => {
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
regionService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -509,6 +519,7 @@ describe("DiscountService", () => {
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -588,6 +599,7 @@ describe("DiscountService", () => {
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
totalsService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -763,6 +775,7 @@ describe("DiscountService", () => {
|
||||
beforeEach(async () => {
|
||||
discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
featureFlagRouter,
|
||||
})
|
||||
const hasReachedLimitMock = jest.fn().mockImplementation(() => false)
|
||||
const isDisabledMock = jest.fn().mockImplementation(() => false)
|
||||
@@ -892,7 +905,9 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
describe("hasReachedLimit", () => {
|
||||
const discountService = new DiscountService({})
|
||||
const discountService = new DiscountService({
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
it("returns true if discount limit is reached", () => {
|
||||
const discount = {
|
||||
@@ -936,7 +951,9 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
describe("isDisabled", () => {
|
||||
const discountService = new DiscountService({})
|
||||
const discountService = new DiscountService({
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
it("returns false if discount not disabled", async () => {
|
||||
const discount = {
|
||||
@@ -972,7 +989,9 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
describe("hasNotStarted", () => {
|
||||
const discountService = new DiscountService({})
|
||||
const discountService = new DiscountService({
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
it("returns true if discount has a future starts_at date", async () => {
|
||||
const discount = {
|
||||
@@ -1008,7 +1027,9 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
describe("hasExpired", () => {
|
||||
const discountService = new DiscountService({})
|
||||
const discountService = new DiscountService({
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
it("returns false if discount has a future ends_at date", async () => {
|
||||
const discount = {
|
||||
@@ -1068,6 +1089,7 @@ describe("DiscountService", () => {
|
||||
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
featureFlagRouter,
|
||||
})
|
||||
discountService.retrieve = retrieveMock
|
||||
|
||||
@@ -1181,6 +1203,7 @@ describe("DiscountService", () => {
|
||||
manager: MockManager,
|
||||
discountConditionRepository,
|
||||
customerService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
it("returns false on undefined customer id", async () => {
|
||||
|
||||
@@ -1,284 +1,520 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import LineItemService from "../line-item"
|
||||
|
||||
describe("LineItemService", () => {
|
||||
describe("create", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
region_id: IdMap.getId("test-region"),
|
||||
}),
|
||||
})
|
||||
|
||||
const regionService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-region"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: (query) => {
|
||||
if (query === IdMap.getId("test-giftcard")) {
|
||||
return {
|
||||
id: IdMap.getId("test-giftcard"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
is_giftcard: true,
|
||||
discountable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
},
|
||||
}
|
||||
},
|
||||
getRegionPrice: () => 100,
|
||||
}
|
||||
|
||||
const pricingService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getProductVariantPricingById: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
}
|
||||
},
|
||||
getProductVariantPricing: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
pricingService,
|
||||
lineItemRepository,
|
||||
productVariantService,
|
||||
regionService,
|
||||
cartRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully create a line item", async () => {
|
||||
await lineItemService.create({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully create a line item with price and quantity", async () => {
|
||||
await lineItemService.create({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully create a line item giftcard", async () => {
|
||||
const line = await lineItemService.generate(
|
||||
IdMap.getId("test-giftcard"),
|
||||
IdMap.getId("test-region"),
|
||||
1
|
||||
)
|
||||
|
||||
await lineItemService.create({
|
||||
...line,
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(2)
|
||||
expect(lineItemRepository.create).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
allow_discounts: false,
|
||||
variant_id: IdMap.getId("test-giftcard"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
is_giftcard: true,
|
||||
should_merge: true,
|
||||
metadata: {},
|
||||
;[true, false].forEach((isTaxInclusiveEnabled) => {
|
||||
describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => {
|
||||
describe("LineItemService", () => {
|
||||
describe("create", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
const cartRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
region_id: IdMap.getId("test-region"),
|
||||
}),
|
||||
})
|
||||
|
||||
const regionService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
lineItemRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates a line item with quantity", async () => {
|
||||
await lineItemService.update(IdMap.getId("test-line-item"), {
|
||||
quantity: 2,
|
||||
has_shipping: true,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
has_shipping: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully updates a line item with metadata", async () => {
|
||||
await lineItemService.update(IdMap.getId("test-line-item"), {
|
||||
metadata: {
|
||||
testKey: "testValue",
|
||||
},
|
||||
})
|
||||
|
||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
metadata: {
|
||||
testKey: "testValue",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("delete", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-region"),
|
||||
}
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
lineItemRepository,
|
||||
})
|
||||
const productVariantService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: (query) => {
|
||||
if (query === IdMap.getId("test-giftcard")) {
|
||||
return {
|
||||
id: IdMap.getId("test-giftcard"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
is_giftcard: true,
|
||||
discountable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
},
|
||||
}
|
||||
},
|
||||
getRegionPrice: () => 100,
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
const pricingService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getProductVariantPricingById: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
}
|
||||
},
|
||||
getProductVariantPricing: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
it("successfully deletes", async () => {
|
||||
await lineItemService.delete(IdMap.getId("test-line-item"))
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
tax_inclusive_pricing: isTaxInclusiveEnabled,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.remove).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.remove).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
pricingService,
|
||||
lineItemRepository,
|
||||
productVariantService,
|
||||
regionService,
|
||||
cartRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully create a line item", async () => {
|
||||
await lineItemService.create({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully create a line item with price and quantity", async () => {
|
||||
await lineItemService.create({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully create a line item giftcard", async () => {
|
||||
const line = await lineItemService.generate(
|
||||
IdMap.getId("test-giftcard"),
|
||||
IdMap.getId("test-region"),
|
||||
1
|
||||
)
|
||||
|
||||
await lineItemService.create({
|
||||
...line,
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
})
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(2)
|
||||
expect(lineItemRepository.create).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
allow_discounts: false,
|
||||
variant_id: IdMap.getId("test-giftcard"),
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
is_giftcard: true,
|
||||
should_merge: true,
|
||||
metadata: {},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
lineItemRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates a line item with quantity", async () => {
|
||||
await lineItemService.update(IdMap.getId("test-line-item"), {
|
||||
quantity: 2,
|
||||
has_shipping: true,
|
||||
})
|
||||
|
||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
has_shipping: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully updates a line item with metadata", async () => {
|
||||
await lineItemService.update(IdMap.getId("test-line-item"), {
|
||||
metadata: {
|
||||
testKey: "testValue",
|
||||
},
|
||||
})
|
||||
|
||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
metadata: {
|
||||
testKey: "testValue",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("delete", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
lineItemRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully deletes", async () => {
|
||||
await lineItemService.delete(IdMap.getId("test-line-item"))
|
||||
|
||||
expect(lineItemRepository.remove).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.remove).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-line-item"),
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
variant: {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
},
|
||||
cart_id: IdMap.getId("test-cart"),
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
unit_price: 50,
|
||||
quantity: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("LineItemService", () => {
|
||||
describe(`tax inclusive pricing tests `, () => {
|
||||
describe("generate", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
region_id: IdMap.getId("test-region"),
|
||||
}),
|
||||
})
|
||||
|
||||
const regionService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-region"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: (query) => {
|
||||
if (query === IdMap.getId("test-giftcard")) {
|
||||
return {
|
||||
id: IdMap.getId("test-giftcard"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
is_giftcard: true,
|
||||
discountable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
},
|
||||
}
|
||||
},
|
||||
getRegionPrice: () => 100,
|
||||
}
|
||||
|
||||
const pricingService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getProductVariantPricingById: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
calculated_price_includes_tax: true,
|
||||
}
|
||||
},
|
||||
getProductVariantPricing: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
calculated_price_includes_tax: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
tax_inclusive_pricing: true,
|
||||
})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
pricingService,
|
||||
lineItemRepository,
|
||||
productVariantService,
|
||||
regionService,
|
||||
cartRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully create a line item with tax inclusive set to true", async () => {
|
||||
await lineItemService.generate(
|
||||
IdMap.getId("test-variant"),
|
||||
IdMap.getId("test-region"),
|
||||
1
|
||||
)
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
unit_price: 100,
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
quantity: 1,
|
||||
allow_discounts: undefined,
|
||||
is_giftcard: undefined,
|
||||
metadata: {},
|
||||
should_merge: true,
|
||||
includes_tax: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("generate", () => {
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
region_id: IdMap.getId("test-region"),
|
||||
}),
|
||||
})
|
||||
|
||||
const regionService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-region"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: (query) => {
|
||||
if (query === IdMap.getId("test-giftcard")) {
|
||||
return {
|
||||
id: IdMap.getId("test-giftcard"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
is_giftcard: true,
|
||||
discountable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: IdMap.getId("test-variant"),
|
||||
title: "Test variant",
|
||||
product: {
|
||||
title: "Test product",
|
||||
thumbnail: "",
|
||||
},
|
||||
}
|
||||
},
|
||||
getRegionPrice: () => 100,
|
||||
}
|
||||
|
||||
const pricingService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getProductVariantPricingById: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
calculated_price_includes_tax: false,
|
||||
}
|
||||
},
|
||||
getProductVariantPricing: () => {
|
||||
return {
|
||||
calculated_price: 100,
|
||||
calculated_price_includes_tax: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
tax_inclusive_pricing: true,
|
||||
})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
pricingService,
|
||||
lineItemRepository,
|
||||
productVariantService,
|
||||
regionService,
|
||||
cartRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully create a line item with tax inclusive set to false", async () => {
|
||||
await lineItemService.generate(
|
||||
IdMap.getId("test-variant"),
|
||||
IdMap.getId("test-region"),
|
||||
1
|
||||
)
|
||||
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
||||
unit_price: 100,
|
||||
title: "Test product",
|
||||
description: "Test variant",
|
||||
thumbnail: "",
|
||||
variant_id: IdMap.getId("test-variant"),
|
||||
quantity: 1,
|
||||
allow_discounts: undefined,
|
||||
is_giftcard: undefined,
|
||||
metadata: {},
|
||||
should_merge: true,
|
||||
includes_tax: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import PriceListService from "../price-list"
|
||||
import { MoneyAmountRepository } from "../../repositories/money-amount"
|
||||
import { RegionServiceMock } from "../__mocks__/region";
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import PriceListService from "../price-list"
|
||||
import { RegionServiceMock } from "../__mocks__/region"
|
||||
|
||||
const priceListRepository = MockRepository({
|
||||
findOne: (q) => {
|
||||
@@ -42,6 +43,7 @@ describe("PriceListService", () => {
|
||||
customerGroupService,
|
||||
priceListRepository,
|
||||
moneyAmountRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -121,16 +123,21 @@ describe("PriceListService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const updateRelatedMoneyAmountRepository = MockRepository()
|
||||
updateRelatedMoneyAmountRepository.create = jest.fn().mockImplementation((rawEntity) => Promise.resolve(rawEntity))
|
||||
updateRelatedMoneyAmountRepository.save = jest.fn().mockImplementation(() => Promise.resolve())
|
||||
updateRelatedMoneyAmountRepository.updatePriceListPrices = (new MoneyAmountRepository()).updatePriceListPrices
|
||||
updateRelatedMoneyAmountRepository.create = jest
|
||||
.fn()
|
||||
.mockImplementation((rawEntity) => Promise.resolve(rawEntity))
|
||||
updateRelatedMoneyAmountRepository.save = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
updateRelatedMoneyAmountRepository.updatePriceListPrices = new MoneyAmountRepository().updatePriceListPrices
|
||||
|
||||
const updateRelatedPriceListService = new PriceListService({
|
||||
manager: MockManager,
|
||||
customerGroupService,
|
||||
priceListRepository,
|
||||
moneyAmountRepository: updateRelatedMoneyAmountRepository,
|
||||
regionService: RegionServiceMock
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
regionService: RegionServiceMock,
|
||||
})
|
||||
|
||||
it("update only existing price lists and related money amount", async () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import RegionService from "../region"
|
||||
import { CreateRegionInput } from "../../types/region"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import {
|
||||
EventBusService,
|
||||
FulfillmentProviderService,
|
||||
PaymentProviderService,
|
||||
StoreService,
|
||||
} from "../index"
|
||||
import { CreateRegionInput } from "../../types/region"
|
||||
import RegionService from "../region"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -87,6 +88,7 @@ describe("RegionService", () => {
|
||||
regionRepository,
|
||||
countryRepository,
|
||||
storeService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
fulfillmentProviderService,
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
@@ -196,6 +198,7 @@ describe("RegionService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
fulfillmentProviderService,
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
@@ -250,6 +253,7 @@ describe("RegionService", () => {
|
||||
currencyRepository,
|
||||
countryRepository,
|
||||
storeService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -312,6 +316,7 @@ describe("RegionService", () => {
|
||||
currencyRepository,
|
||||
countryRepository,
|
||||
storeService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -367,6 +372,7 @@ describe("RegionService", () => {
|
||||
paymentProviderRepository: ppRepository,
|
||||
currencyRepository,
|
||||
countryRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
storeService,
|
||||
})
|
||||
|
||||
@@ -427,6 +433,7 @@ describe("RegionService", () => {
|
||||
paymentProviderRepository: ppRepository,
|
||||
currencyRepository,
|
||||
countryRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
storeService,
|
||||
})
|
||||
|
||||
@@ -472,6 +479,7 @@ describe("RegionService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
} as any)
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -507,6 +515,7 @@ describe("RegionService", () => {
|
||||
paymentProviderService,
|
||||
fulfillmentProviderRepository: fpRepository,
|
||||
paymentProviderRepository: ppRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
currencyRepository,
|
||||
countryRepository,
|
||||
storeService,
|
||||
@@ -569,6 +578,7 @@ describe("RegionService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
fulfillmentProviderService,
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
@@ -626,6 +636,7 @@ describe("RegionService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
} as any)
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -661,6 +672,7 @@ describe("RegionService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
} as any)
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ShippingOptionService from "../shipping-option"
|
||||
import { FlagRouter } from "../../utils/flag-router";
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing";
|
||||
|
||||
describe("ShippingOptionService", () => {
|
||||
describe("retrieve", () => {
|
||||
@@ -13,6 +15,7 @@ describe("ShippingOptionService", () => {
|
||||
const optionService = new ShippingOptionService({
|
||||
manager: MockManager,
|
||||
shippingOptionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
it("successfully gets shipping option", async () => {
|
||||
@@ -60,6 +63,7 @@ describe("ShippingOptionService", () => {
|
||||
shippingOptionRepository,
|
||||
shippingOptionRequirementRepository,
|
||||
fulfillmentProviderService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -214,6 +218,7 @@ describe("ShippingOptionService", () => {
|
||||
const optionService = new ShippingOptionService({
|
||||
manager: MockManager,
|
||||
shippingOptionRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -262,6 +267,7 @@ describe("ShippingOptionService", () => {
|
||||
manager: MockManager,
|
||||
shippingOptionRepository,
|
||||
shippingOptionRequirementRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -311,6 +317,7 @@ describe("ShippingOptionService", () => {
|
||||
const optionService = new ShippingOptionService({
|
||||
manager: MockManager,
|
||||
shippingOptionRequirementRepository,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -363,6 +370,7 @@ describe("ShippingOptionService", () => {
|
||||
shippingOptionRequirementRepository,
|
||||
fulfillmentProviderService,
|
||||
regionService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -526,6 +534,7 @@ describe("ShippingOptionService", () => {
|
||||
shippingOptionRepository,
|
||||
totalsService,
|
||||
fulfillmentProviderService: providerService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -589,4 +598,68 @@ describe("ShippingOptionService", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] createShippingMethod", () => {
|
||||
const option = (id) => ({
|
||||
id,
|
||||
region_id: IdMap.getId("region"),
|
||||
price_type: "flat_rate",
|
||||
amount: 10,
|
||||
includes_tax: true,
|
||||
data: {
|
||||
something: "yes",
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
type: "min_subtotal",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
const shippingOptionRepository = MockRepository({
|
||||
findOne: (q) => {
|
||||
switch (q.where.id) {
|
||||
default:
|
||||
return Promise.resolve(option(q.where.id))
|
||||
}
|
||||
},
|
||||
})
|
||||
const shippingMethodRepository = MockRepository({ create: (r) => r })
|
||||
const totalsService = {
|
||||
getSubtotal: (c) => {
|
||||
return c.subtotal
|
||||
},
|
||||
}
|
||||
|
||||
const providerService = {
|
||||
validateFulfillmentData: jest
|
||||
.fn()
|
||||
.mockImplementation((r) => Promise.resolve(r.data)),
|
||||
getPrice: (d) => d.price,
|
||||
}
|
||||
|
||||
const optionService = new ShippingOptionService({
|
||||
manager: MockManager,
|
||||
shippingMethodRepository,
|
||||
shippingOptionRepository,
|
||||
totalsService,
|
||||
fulfillmentProviderService: providerService,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: true
|
||||
}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should create a shipping method that also includes the taxes", async () => {
|
||||
await optionService.createShippingMethod("random_id", {}, { price: 10 })
|
||||
expect(shippingMethodRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includes_tax: true
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import TotalsService from "../totals"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { calculatePriceTaxAmount } from "../../utils"
|
||||
|
||||
const discounts = {
|
||||
total10Percent: {
|
||||
@@ -83,11 +87,21 @@ const applyDiscount = (cart, discount) => {
|
||||
const calculateAdjustment = (cart, lineItem, discount) => {
|
||||
let amount = discount.rule.value * lineItem.quantity
|
||||
|
||||
let lineItemPrice = lineItem.unit_price * lineItem.quantity
|
||||
const taxAmountIncludedInPrice = !lineItem.includes_tax
|
||||
? 0
|
||||
: Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: lineItem.unit_price,
|
||||
taxRate: cart.tax_rate / 100,
|
||||
includesTax: lineItem.includes_tax,
|
||||
})
|
||||
)
|
||||
let price = lineItem.unit_price - taxAmountIncludedInPrice
|
||||
const lineItemPrice = price * lineItem.quantity
|
||||
|
||||
if (discount.rule.type === "fixed" && discount.rule.allocation === "total") {
|
||||
let subtotal = cart.items.reduce(
|
||||
(total, item) => total + item.unit_price * item.quantity,
|
||||
(total, item) => total + price * item.quantity,
|
||||
0
|
||||
)
|
||||
const nominator = Math.min(discount.rule.value, subtotal)
|
||||
@@ -99,13 +113,20 @@ const calculateAdjustment = (cart, lineItem, discount) => {
|
||||
}
|
||||
|
||||
describe("TotalsService", () => {
|
||||
const getTaxLinesMock = jest.fn(() => Promise.resolve([{ id: "line1" }]))
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: false,
|
||||
})
|
||||
|
||||
const container = {
|
||||
taxProviderService: {
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
getTaxLines: getTaxLinesMock,
|
||||
},
|
||||
taxCalculationStrategy: {},
|
||||
featureFlagRouter,
|
||||
}
|
||||
|
||||
describe("getAllocationItemDiscounts", () => {
|
||||
@@ -278,7 +299,7 @@ describe("TotalsService", () => {
|
||||
it("calculate total percentage discount", async () => {
|
||||
discountCart.discounts.push(discounts.total10Percent)
|
||||
let cart = applyDiscount(discountCart, discounts.total10Percent)
|
||||
res = totalsService.getDiscountTotal(cart)
|
||||
res = await totalsService.getDiscountTotal(cart)
|
||||
|
||||
expect(res).toEqual(28)
|
||||
})
|
||||
@@ -288,7 +309,7 @@ describe("TotalsService", () => {
|
||||
it("calculate item fixed discount", async () => {
|
||||
discountCart.discounts.push(discounts.item2Fixed)
|
||||
let cart = applyDiscount(discountCart, discounts.item2Fixed)
|
||||
res = totalsService.getDiscountTotal(cart)
|
||||
res = await totalsService.getDiscountTotal(cart)
|
||||
|
||||
expect(res).toEqual(40)
|
||||
})
|
||||
@@ -296,7 +317,7 @@ describe("TotalsService", () => {
|
||||
it("calculate item percentage discount", async () => {
|
||||
discountCart.discounts.push(discounts.item10Percent)
|
||||
let cart = applyDiscount(discountCart, discounts.item10Percent)
|
||||
res = totalsService.getDiscountTotal(cart)
|
||||
res = await totalsService.getDiscountTotal(cart)
|
||||
|
||||
expect(res).toEqual(28)
|
||||
})
|
||||
@@ -304,26 +325,26 @@ describe("TotalsService", () => {
|
||||
it("calculate total fixed discount", async () => {
|
||||
discountCart.discounts.push(discounts.total10Fixed)
|
||||
let cart = applyDiscount(discountCart, discounts.total10Fixed)
|
||||
res = totalsService.getDiscountTotal(cart)
|
||||
res = await totalsService.getDiscountTotal(cart)
|
||||
|
||||
expect(res).toEqual(10)
|
||||
})
|
||||
|
||||
it("ignores discount if expired", async () => {
|
||||
discountCart.discounts.push(discounts.expiredDiscount)
|
||||
res = totalsService.getDiscountTotal(discountCart)
|
||||
res = await totalsService.getDiscountTotal(discountCart)
|
||||
|
||||
expect(res).toEqual(0)
|
||||
})
|
||||
|
||||
it("returns 0 if no discounts are applied", async () => {
|
||||
res = totalsService.getDiscountTotal(discountCart)
|
||||
res = await totalsService.getDiscountTotal(discountCart)
|
||||
|
||||
expect(res).toEqual(0)
|
||||
})
|
||||
|
||||
it("returns 0 if no items are in cart", async () => {
|
||||
res = totalsService.getDiscountTotal({
|
||||
res = await totalsService.getDiscountTotal({
|
||||
items: [],
|
||||
discounts: [discounts.total10Fixed],
|
||||
})
|
||||
@@ -385,7 +406,7 @@ describe("TotalsService", () => {
|
||||
})
|
||||
|
||||
it("calculates refund", async () => {
|
||||
res = totalsService.getRefundTotal(orderToRefund, [
|
||||
res = await totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
@@ -447,7 +468,7 @@ describe("TotalsService", () => {
|
||||
it("calculates refund with item fixed discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
|
||||
res = totalsService.getRefundTotal(order, [
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
@@ -467,7 +488,7 @@ describe("TotalsService", () => {
|
||||
it("calculates refund with item percentage discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item10Percent)
|
||||
let order = applyDiscount(orderToRefund, discounts.item10Percent)
|
||||
res = totalsService.getRefundTotal(order, [
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
@@ -485,8 +506,9 @@ describe("TotalsService", () => {
|
||||
})
|
||||
|
||||
it("throws if line items to return is not in order", async () => {
|
||||
const work = () =>
|
||||
totalsService.getRefundTotal(orderToRefund, [
|
||||
let errMsg
|
||||
await totalsService
|
||||
.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "notInOrder",
|
||||
unit_price: 123,
|
||||
@@ -498,14 +520,213 @@ describe("TotalsService", () => {
|
||||
quantity: 1,
|
||||
},
|
||||
])
|
||||
.catch((e) => (errMsg = e.message))
|
||||
|
||||
expect(work).toThrow("Line item does not exist on order")
|
||||
expect(errMsg).toBe("Line item does not exist on order")
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] getRefundTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
...container,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const orderToRefund = {
|
||||
id: "refund-order",
|
||||
tax_rate: 25,
|
||||
items: [
|
||||
{
|
||||
id: "line",
|
||||
unit_price: 125,
|
||||
includes_tax: true,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp1",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: "non-discount",
|
||||
unit_price: 100,
|
||||
allow_discounts: false,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 1,
|
||||
returned_quantity: 0,
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
region_id: "fr",
|
||||
discounts: [],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
orderToRefund.discounts = []
|
||||
})
|
||||
|
||||
it("calculates refund", async () => {
|
||||
res = await totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "product2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
metadata: {},
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1250)
|
||||
})
|
||||
|
||||
it("calculates refund with line that includes tax", async () => {
|
||||
res = await totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line",
|
||||
unit_price: 125,
|
||||
includes_tax: true,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "product2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
metadata: {},
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1250)
|
||||
})
|
||||
|
||||
it("calculates refund with item fixed discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1225)
|
||||
})
|
||||
|
||||
it("calculates refund with item fixed discount and a line that includes tax", async () => {
|
||||
orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line",
|
||||
unit_price: 125,
|
||||
includes_tax: true,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1225)
|
||||
})
|
||||
|
||||
it("calculates refund with item percentage discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item10Percent)
|
||||
let order = applyDiscount(orderToRefund, discounts.item10Percent)
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1125)
|
||||
})
|
||||
|
||||
it("calculates refund with item percentage discount and a line that includes tax", async () => {
|
||||
orderToRefund.discounts.push(discounts.item10Percent)
|
||||
let order = applyDiscount(orderToRefund, discounts.item10Percent)
|
||||
res = await totalsService.getRefundTotal(order, [
|
||||
{
|
||||
id: "line",
|
||||
unit_price: 125,
|
||||
includes_tax: true,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1125)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getShippingTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService(container)
|
||||
const getTaxLinesMock = jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{ shipping_method_id: IdMap.getId("expensiveShipping") },
|
||||
])
|
||||
)
|
||||
const calculateMock = jest.fn(() => Promise.resolve(20))
|
||||
|
||||
const totalsService = new TotalsService({
|
||||
...container,
|
||||
taxProviderService: {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
getTaxLines: getTaxLinesMock,
|
||||
},
|
||||
taxCalculationStrategy: {
|
||||
calculate: calculateMock,
|
||||
},
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -515,7 +736,7 @@ describe("TotalsService", () => {
|
||||
const order = {
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
@@ -526,11 +747,12 @@ describe("TotalsService", () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
res = totalsService.getShippingTotal(order)
|
||||
const total = await totalsService.getShippingTotal(order)
|
||||
|
||||
expect(res).toEqual(100)
|
||||
expect(total).toEqual(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTaxTotal", () => {
|
||||
let res
|
||||
let totalsService
|
||||
@@ -541,7 +763,7 @@ describe("TotalsService", () => {
|
||||
|
||||
const cradle = {
|
||||
taxProviderService: {
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
getTaxLines: getTaxLinesMock,
|
||||
@@ -549,6 +771,7 @@ describe("TotalsService", () => {
|
||||
taxCalculationStrategy: {
|
||||
calculate: calculateMock,
|
||||
},
|
||||
featureFlagRouter,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -597,13 +820,14 @@ describe("TotalsService", () => {
|
||||
|
||||
expect(res).toEqual(20)
|
||||
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(2)
|
||||
expect(getAllocationMapMock).toHaveBeenNthCalledWith(1, order, {})
|
||||
|
||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(0)
|
||||
|
||||
expect(calculateMock).toHaveBeenCalledTimes(1)
|
||||
expect(calculateMock).toHaveBeenCalledWith(
|
||||
expect(calculateMock).toHaveBeenCalledTimes(3)
|
||||
expect(calculateMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
order.items,
|
||||
[{ id: "orderline1" }],
|
||||
{
|
||||
@@ -652,11 +876,15 @@ describe("TotalsService", () => {
|
||||
|
||||
expect(res).toEqual(20)
|
||||
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(2)
|
||||
expect(getAllocationMapMock).toHaveBeenNthCalledWith(2, order, {
|
||||
exclude_discounts: undefined,
|
||||
exclude_gift_cards: true,
|
||||
})
|
||||
|
||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(1)
|
||||
expect(getTaxLinesMock).toHaveBeenCalledWith(
|
||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(2)
|
||||
expect(getTaxLinesMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[{ quantity: 2, unit_price: 20 }],
|
||||
{
|
||||
shipping_address: order.shipping_address,
|
||||
@@ -726,4 +954,137 @@ describe("TotalsService", () => {
|
||||
expect(res).toEqual(175)
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] getTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
...container,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: true,
|
||||
}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calculates total", async () => {
|
||||
const order = {
|
||||
region: {
|
||||
tax_rate: 25,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
unit_price: 20,
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
unit_price: 25,
|
||||
quantity: 2,
|
||||
includes_tax: true,
|
||||
},
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
data: {
|
||||
extra: "hi",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
const getTaxTotalMock = jest.fn(() => Promise.resolve(45))
|
||||
totalsService.getTaxTotal = getTaxTotalMock
|
||||
res = await totalsService.getTotal(order)
|
||||
|
||||
expect(getTaxTotalMock).toHaveBeenCalledTimes(1)
|
||||
expect(getTaxTotalMock).toHaveBeenCalledWith(order, undefined)
|
||||
|
||||
expect(res).toEqual(185)
|
||||
})
|
||||
})
|
||||
|
||||
describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] getShippingTotal ", () => {
|
||||
const shippingMethodData = {
|
||||
id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 120,
|
||||
tax_lines: [{ shipping_method_id: IdMap.getId("expensiveShipping") }],
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
data: {
|
||||
extra: "hi",
|
||||
},
|
||||
}
|
||||
const calculateMock = jest.fn(() => Promise.resolve(20))
|
||||
const totalsService = new TotalsService({
|
||||
...container,
|
||||
taxCalculationStrategy: {
|
||||
calculate: calculateMock,
|
||||
},
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: true,
|
||||
}),
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calculates total with tax lines and being tax inclusive", async () => {
|
||||
const order = {
|
||||
object: "order",
|
||||
shipping_methods: [
|
||||
{
|
||||
...shippingMethodData,
|
||||
includes_tax: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const total = await totalsService.getShippingTotal(order)
|
||||
|
||||
expect(total).toEqual(100)
|
||||
})
|
||||
|
||||
it("calculates total with tax lines and not being tax inclusive", async () => {
|
||||
const order = {
|
||||
object: "order",
|
||||
shipping_methods: [
|
||||
{
|
||||
...shippingMethodData,
|
||||
price: 100,
|
||||
includes_tax: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const total = await totalsService.getShippingTotal(order)
|
||||
|
||||
expect(total).toEqual(100)
|
||||
})
|
||||
|
||||
it("calculates total with the old system and not being tax inclusive", async () => {
|
||||
const order = {
|
||||
object: "order",
|
||||
tax_rate: 20,
|
||||
shipping_methods: [
|
||||
{
|
||||
...shippingMethodData,
|
||||
price: 100,
|
||||
includes_tax: false,
|
||||
tax_lines: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const total = await totalsService.getShippingTotal(order)
|
||||
|
||||
expect(total).toEqual(100)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -243,11 +243,15 @@ class CartService extends TransactionBaseService {
|
||||
break
|
||||
}
|
||||
case "shipping_total": {
|
||||
totals.shipping_total = this.totalsService_.getShippingTotal(cart)
|
||||
totals.shipping_total = await this.totalsService_.getShippingTotal(
|
||||
cart
|
||||
)
|
||||
break
|
||||
}
|
||||
case "discount_total":
|
||||
totals.discount_total = this.totalsService_.getDiscountTotal(cart)
|
||||
totals.discount_total = await this.totalsService_.getDiscountTotal(
|
||||
cart
|
||||
)
|
||||
break
|
||||
case "tax_total":
|
||||
totals.tax_total = await this.totalsService_.getTaxTotal(
|
||||
@@ -256,13 +260,15 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
break
|
||||
case "gift_card_total": {
|
||||
const giftCardBreakdown = this.totalsService_.getGiftCardTotal(cart)
|
||||
const giftCardBreakdown = await this.totalsService_.getGiftCardTotal(
|
||||
cart
|
||||
)
|
||||
totals.gift_card_total = giftCardBreakdown.total
|
||||
totals.gift_card_tax_total = giftCardBreakdown.tax_total
|
||||
break
|
||||
}
|
||||
case "subtotal":
|
||||
totals.subtotal = this.totalsService_.getSubtotal(cart)
|
||||
totals.subtotal = await this.totalsService_.getSubtotal(cart)
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -518,7 +524,7 @@ class CartService extends TransactionBaseService {
|
||||
.delete(lineItem.id)
|
||||
|
||||
const result = await this.retrieve(cartId, {
|
||||
relations: ["items", "discounts", "discounts.rule"],
|
||||
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||
})
|
||||
|
||||
await this.refreshAdjustments_(result)
|
||||
@@ -686,7 +692,7 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
const result = await this.retrieve(cartId, {
|
||||
relations: ["items", "discounts", "discounts.rule"],
|
||||
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||
})
|
||||
|
||||
await this.refreshAdjustments_(result)
|
||||
@@ -748,7 +754,7 @@ class CartService extends TransactionBaseService {
|
||||
.update(lineItemId, lineItemUpdate)
|
||||
|
||||
const updatedCart = await this.retrieve(cartId, {
|
||||
relations: ["items", "discounts", "discounts.rule"],
|
||||
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||
})
|
||||
|
||||
await this.refreshAdjustments_(updatedCart)
|
||||
@@ -919,14 +925,14 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
const hasFreeShipping = cart.discounts.some(
|
||||
({ rule }) => rule?.type === "free_shipping"
|
||||
({ rule }) => rule?.type === DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
|
||||
// if we previously had a free shipping discount and then removed it,
|
||||
// we need to update shipping methods to original price
|
||||
if (
|
||||
previousDiscounts.some(
|
||||
({ rule }) => rule.type === "free_shipping"
|
||||
({ rule }) => rule.type === DiscountRuleType.FREE_SHIPPING
|
||||
) &&
|
||||
!hasFreeShipping
|
||||
) {
|
||||
@@ -1229,7 +1235,7 @@ class CartService extends TransactionBaseService {
|
||||
default:
|
||||
if (!sawNotShipping) {
|
||||
sawNotShipping = true
|
||||
if (rule?.type !== "free_shipping") {
|
||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING) {
|
||||
return discount
|
||||
}
|
||||
return discountToParse
|
||||
@@ -1245,7 +1251,7 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
// ignore if free shipping
|
||||
if (rule?.type !== "free_shipping" && cart?.items) {
|
||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING && cart?.items) {
|
||||
await this.refreshAdjustments_(cart)
|
||||
}
|
||||
}
|
||||
@@ -1270,7 +1276,11 @@ class CartService extends TransactionBaseService {
|
||||
],
|
||||
})
|
||||
|
||||
if (cart.discounts.some(({ rule }) => rule.type === "free_shipping")) {
|
||||
if (
|
||||
cart.discounts.some(
|
||||
({ rule }) => rule.type === DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
) {
|
||||
await this.adjustFreeShipping_(cart, false)
|
||||
}
|
||||
|
||||
@@ -1770,7 +1780,7 @@ class CartService extends TransactionBaseService {
|
||||
// if cart has freeshipping, adjust price
|
||||
if (
|
||||
updatedCart.discounts.some(
|
||||
({ rule }) => rule.type === "free_shipping"
|
||||
({ rule }) => rule.type === DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
) {
|
||||
await this.adjustFreeShipping_(updatedCart, true)
|
||||
@@ -2107,7 +2117,7 @@ class CartService extends TransactionBaseService {
|
||||
],
|
||||
})
|
||||
|
||||
const calculationContext = this.totalsService_
|
||||
const calculationContext = await this.totalsService_
|
||||
.withTransaction(transactionManager)
|
||||
.getCalculationContext(cart)
|
||||
|
||||
|
||||
@@ -381,7 +381,9 @@ export default class ClaimService extends TransactionBaseService {
|
||||
const result: ClaimOrder = await claimRepo.save(created)
|
||||
|
||||
if (result.additional_items && result.additional_items.length) {
|
||||
const calcContext = this.totalsService_.getCalculationContext(order)
|
||||
const calcContext = await this.totalsService_.getCalculationContext(
|
||||
order
|
||||
)
|
||||
const lineItems = await lineItemServiceTx.list({
|
||||
id: result.additional_items.map((i) => i.id),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { Currency } from "../models"
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import { FindConfig, Selector } from "../types/common"
|
||||
import { UpdateCurrencyInput } from "../types/currency"
|
||||
import { buildQuery } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import EventBusService from "./event-bus"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
currencyRepository: typeof CurrencyRepository
|
||||
eventBusService: EventBusService
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
export default class CurrencyService extends TransactionBaseService {
|
||||
static readonly Events = {
|
||||
UPDATED: "currency.updated",
|
||||
}
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly currencyRepository_: typeof CurrencyRepository
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
currencyRepository,
|
||||
eventBusService,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
super({ manager })
|
||||
this.manager_ = manager
|
||||
this.currencyRepository_ = currencyRepository
|
||||
this.eventBusService_ = eventBusService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currency
|
||||
* @param code - The code of the currency that must be retrieve
|
||||
* @return The currency
|
||||
*/
|
||||
async retrieveByCode(code: string): Promise<Currency | never> {
|
||||
const currencyRepo = this.manager_.getCustomRepository(
|
||||
this.currencyRepository_
|
||||
)
|
||||
|
||||
code = code.toLowerCase()
|
||||
const currency = await currencyRepo.findOne({
|
||||
where: { code },
|
||||
})
|
||||
|
||||
if (!currency) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Currency with code: ${code} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return currency
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists currencies based on the provided parameters and includes the count of
|
||||
* currencies that match the query.
|
||||
* @param selector - an object that defines rules to filter currencies
|
||||
* by
|
||||
* @param config - object that defines the scope for what should be
|
||||
* returned
|
||||
* @return an array containing the currencies as
|
||||
* the first element and the total count of products that matches the query
|
||||
* as the second element.
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: Selector<Currency>,
|
||||
config: FindConfig<Currency> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
}
|
||||
): Promise<[Currency[], number]> {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.currencyRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return await productRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a currency
|
||||
* @param code - The code of the currency to update
|
||||
* @param data - The data that must be updated on the currency
|
||||
* @return The updated currency
|
||||
*/
|
||||
async update(
|
||||
code: string,
|
||||
data: UpdateCurrencyInput
|
||||
): Promise<Currency | undefined | never> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const currency = await this.retrieveByCode(code)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof data.includes_tax !== "undefined") {
|
||||
currency.includes_tax = data.includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
const currencyRepo = transactionManager.getCustomRepository(
|
||||
this.currencyRepository_
|
||||
)
|
||||
await currencyRepo.save(currency)
|
||||
|
||||
await this.eventBusService_.emit(CurrencyService.Events.UPDATED, {
|
||||
code,
|
||||
})
|
||||
|
||||
return currency
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import DiscountConditionService from "./discount-condition"
|
||||
import CustomerService from "./customer"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate discounts.
|
||||
@@ -58,6 +60,7 @@ class DiscountService extends TransactionBaseService {
|
||||
protected readonly productService_: ProductService
|
||||
protected readonly regionService_: RegionService
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -71,6 +74,7 @@ class DiscountService extends TransactionBaseService {
|
||||
regionService,
|
||||
customerService,
|
||||
eventBusService,
|
||||
featureFlagRouter,
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -86,6 +90,7 @@ class DiscountService extends TransactionBaseService {
|
||||
this.regionService_ = regionService
|
||||
this.customerService_ = customerService
|
||||
this.eventBus_ = eventBusService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,7 +584,23 @@ class DiscountService extends TransactionBaseService {
|
||||
|
||||
const { type, value, allocation } = discount.rule
|
||||
|
||||
const fullItemPrice = lineItem.unit_price * lineItem.quantity
|
||||
let fullItemPrice = lineItem.unit_price * lineItem.quantity
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) &&
|
||||
lineItem.includes_tax
|
||||
) {
|
||||
const lineItemTotals = await this.totalsService_.getLineItemTotals(
|
||||
lineItem,
|
||||
cart,
|
||||
{
|
||||
include_tax: true,
|
||||
exclude_gift_cards: true,
|
||||
}
|
||||
)
|
||||
fullItemPrice = lineItemTotals.subtotal
|
||||
}
|
||||
|
||||
if (type === DiscountRuleType.PERCENTAGE) {
|
||||
adjustment = Math.round((fullItemPrice / 100) * value)
|
||||
@@ -590,12 +611,11 @@ class DiscountService extends TransactionBaseService {
|
||||
// when a fixed discount should be applied to the total,
|
||||
// we create line adjustments for each item with an amount
|
||||
// relative to the subtotal
|
||||
const subtotal = this.totalsService_.getSubtotal(cart, {
|
||||
const subtotal = await this.totalsService_.getSubtotal(cart, {
|
||||
excludeNonDiscounts: true,
|
||||
})
|
||||
const nominator = Math.min(value, subtotal)
|
||||
const itemRelativeToSubtotal = lineItem.unit_price / subtotal
|
||||
const totalItemPercentage = itemRelativeToSubtotal * lineItem.quantity
|
||||
const totalItemPercentage = fullItemPrice / subtotal
|
||||
adjustment = Math.round(nominator * totalItemPercentage)
|
||||
} else {
|
||||
adjustment = value * lineItem.quantity
|
||||
|
||||
@@ -3,6 +3,7 @@ export { default as BatchJobService } from "./batch-job"
|
||||
export { default as CartService } from "./cart"
|
||||
export { default as ClaimItemService } from "./claim-item"
|
||||
export { default as ClaimService } from "./claim"
|
||||
export { default as CurrencyService } from "./currency"
|
||||
export { default as CustomShippingOptionService } from "./custom-shipping-option"
|
||||
export { default as CustomerGroupService } from "./customer-group"
|
||||
export { default as CustomerService } from "./customer"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { Cart } from "../models/cart"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { LineItemAdjustment } from "../models/line-item-adjustment"
|
||||
import { ProductVariant } from "../models/product-variant"
|
||||
import {
|
||||
Cart,
|
||||
DiscountRuleType,
|
||||
LineItem,
|
||||
LineItemAdjustment,
|
||||
ProductVariant,
|
||||
} from "../models"
|
||||
import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment"
|
||||
@@ -70,8 +73,9 @@ class LineItemAdjustmentService extends BaseService {
|
||||
id: string,
|
||||
config: FindConfig<LineItemAdjustment> = {}
|
||||
): Promise<LineItemAdjustment> {
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
||||
this.manager_.getCustomRepository(this.lineItemAdjustmentRepo_)
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = this.manager_.getCustomRepository(
|
||||
this.lineItemAdjustmentRepo_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_({ id }, config)
|
||||
const lineItemAdjustment = await lineItemAdjustmentRepo.findOne(query)
|
||||
@@ -93,8 +97,9 @@ class LineItemAdjustmentService extends BaseService {
|
||||
*/
|
||||
async create(data: Partial<LineItemAdjustment>): Promise<LineItemAdjustment> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||
this.lineItemAdjustmentRepo_
|
||||
)
|
||||
|
||||
const lineItemAdjustment = lineItemAdjustmentRepo.create(data)
|
||||
|
||||
@@ -113,8 +118,9 @@ class LineItemAdjustmentService extends BaseService {
|
||||
data: Partial<LineItemAdjustment>
|
||||
): Promise<LineItemAdjustment> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||
this.lineItemAdjustmentRepo_
|
||||
)
|
||||
|
||||
const lineItemAdjustment = await this.retrieve(id)
|
||||
|
||||
@@ -163,8 +169,9 @@ class LineItemAdjustmentService extends BaseService {
|
||||
selectorOrId: string | FilterableLineItemAdjustmentProps
|
||||
): Promise<void> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||
this.lineItemAdjustmentRepo_
|
||||
)
|
||||
|
||||
if (typeof selectorOrId === "string") {
|
||||
return await this.delete({ id: selectorOrId })
|
||||
@@ -206,7 +213,7 @@ class LineItemAdjustmentService extends BaseService {
|
||||
}
|
||||
|
||||
const [discount] = cart.discounts.filter(
|
||||
(d) => d.rule.type !== "free_shipping"
|
||||
(d) => d.rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
|
||||
// if no discount is applied to the cart then return
|
||||
|
||||
@@ -2,21 +2,23 @@ import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { DeepPartial } from "typeorm/common/DeepPartial"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { LineItemTaxLine } from "../models"
|
||||
import { Cart } from "../models/cart"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { LineItemAdjustment } from "../models/line-item-adjustment"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
import { LineItemRepository } from "../repositories/line-item"
|
||||
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import {
|
||||
PricingService,
|
||||
ProductService,
|
||||
RegionService,
|
||||
ProductVariantService,
|
||||
RegionService,
|
||||
} from "./index"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||
import { Cart } from "../models/cart"
|
||||
import { LineItemAdjustment } from "../models/line-item-adjustment"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { LineItemTaxLine } from "../models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -28,6 +30,7 @@ type InjectedDependencies = {
|
||||
pricingService: PricingService
|
||||
regionService: RegionService
|
||||
lineItemAdjustmentService: LineItemAdjustmentService
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +44,9 @@ class LineItemService extends BaseService {
|
||||
protected readonly cartRepository_: typeof CartRepository
|
||||
protected readonly productVariantService_: ProductVariantService
|
||||
protected readonly productService_: ProductService
|
||||
protected readonly pricingService_: PricingService
|
||||
protected readonly regionService_: RegionService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||
|
||||
constructor({
|
||||
@@ -54,6 +59,7 @@ class LineItemService extends BaseService {
|
||||
regionService,
|
||||
cartRepository,
|
||||
lineItemAdjustmentService,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
super()
|
||||
|
||||
@@ -66,6 +72,7 @@ class LineItemService extends BaseService {
|
||||
this.regionService_ = regionService
|
||||
this.cartRepository_ = cartRepository
|
||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): LineItemService {
|
||||
@@ -83,6 +90,7 @@ class LineItemService extends BaseService {
|
||||
regionService: this.regionService_,
|
||||
cartRepository: this.cartRepository_,
|
||||
lineItemAdjustmentService: this.lineItemAdjustmentService_,
|
||||
featureFlagRouter: this.featureFlagRouter_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -197,6 +205,7 @@ class LineItemService extends BaseService {
|
||||
quantity: number,
|
||||
context: {
|
||||
unit_price?: number
|
||||
includes_tax?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
customer_id?: string
|
||||
cart?: Cart
|
||||
@@ -216,6 +225,9 @@ class LineItemService extends BaseService {
|
||||
])
|
||||
|
||||
let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price
|
||||
|
||||
let unitPriceIncludesTax = false
|
||||
|
||||
let shouldMerge = false
|
||||
|
||||
if (context.unit_price === undefined || context.unit_price === null) {
|
||||
@@ -228,7 +240,10 @@ class LineItemService extends BaseService {
|
||||
customer_id: context?.customer_id,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
unit_price = variantPricing.calculated_price
|
||||
|
||||
unitPriceIncludesTax = !!variantPricing.calculated_price_includes_tax
|
||||
|
||||
unit_price = variantPricing.calculated_price ?? undefined
|
||||
}
|
||||
|
||||
const rawLineItem: Partial<LineItem> = {
|
||||
@@ -244,6 +259,14 @@ class LineItemService extends BaseService {
|
||||
should_merge: shouldMerge,
|
||||
}
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
rawLineItem.includes_tax = unitPriceIncludesTax
|
||||
}
|
||||
|
||||
const lineItemRepo = transactionManager.getCustomRepository(
|
||||
this.lineItemRepository_
|
||||
)
|
||||
|
||||
@@ -164,8 +164,9 @@ class OrderService extends TransactionBaseService {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||
config
|
||||
)
|
||||
|
||||
if (select && select.length) {
|
||||
query.select = select
|
||||
@@ -233,8 +234,9 @@ class OrderService extends TransactionBaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||
config
|
||||
)
|
||||
|
||||
if (select && select.length) {
|
||||
query.select = select
|
||||
@@ -252,7 +254,9 @@ class OrderService extends TransactionBaseService {
|
||||
return [orders, count]
|
||||
}
|
||||
|
||||
protected transformQueryForTotals(config: FindConfig<Order>): {
|
||||
protected transformQueryForTotals(
|
||||
config: FindConfig<Order>
|
||||
): {
|
||||
relations: string[] | undefined
|
||||
select: FindConfig<Order>["select"]
|
||||
totalsToSelect: FindConfig<Order>["select"]
|
||||
@@ -333,8 +337,9 @@ class OrderService extends TransactionBaseService {
|
||||
): Promise<Order> {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { id: orderId },
|
||||
@@ -373,8 +378,9 @@ class OrderService extends TransactionBaseService {
|
||||
): Promise<Order> {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { cart_id: cartId },
|
||||
@@ -412,8 +418,9 @@ class OrderService extends TransactionBaseService {
|
||||
): Promise<Order> {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { external_id: externalId },
|
||||
@@ -851,8 +858,9 @@ class OrderService extends TransactionBaseService {
|
||||
.withTransaction(manager)
|
||||
.createShippingMethod(optionId, data ?? {}, { order, ...config })
|
||||
|
||||
const shippingOptionServiceTx =
|
||||
this.shippingOptionService_.withTransaction(manager)
|
||||
const shippingOptionServiceTx = this.shippingOptionService_.withTransaction(
|
||||
manager
|
||||
)
|
||||
|
||||
const methods = [newMethod]
|
||||
if (shipping_methods.length) {
|
||||
@@ -1023,8 +1031,9 @@ class OrderService extends TransactionBaseService {
|
||||
await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(manager)
|
||||
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
|
||||
manager
|
||||
)
|
||||
for (const p of order.payments) {
|
||||
await paymentProviderServiceTx.cancelPayment(p)
|
||||
}
|
||||
@@ -1064,8 +1073,9 @@ class OrderService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(manager)
|
||||
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
|
||||
manager
|
||||
)
|
||||
|
||||
const payments: Payment[] = []
|
||||
for (const p of order.payments) {
|
||||
@@ -1218,7 +1228,7 @@ class OrderService extends TransactionBaseService {
|
||||
const fulfillments = await this.fulfillmentService_
|
||||
.withTransaction(manager)
|
||||
.createFulfillment(
|
||||
order as unknown as CreateFulfillmentOrder,
|
||||
(order as unknown) as CreateFulfillmentOrder,
|
||||
itemsToFulfill,
|
||||
{
|
||||
metadata,
|
||||
@@ -1429,17 +1439,23 @@ class OrderService extends TransactionBaseService {
|
||||
for (const totalField of totalsFields) {
|
||||
switch (totalField) {
|
||||
case "shipping_total": {
|
||||
order.shipping_total = this.totalsService_.getShippingTotal(order)
|
||||
order.shipping_total = await this.totalsService_.getShippingTotal(
|
||||
order
|
||||
)
|
||||
break
|
||||
}
|
||||
case "gift_card_total": {
|
||||
const giftCardBreakdown = this.totalsService_.getGiftCardTotal(order)
|
||||
const giftCardBreakdown = await this.totalsService_.getGiftCardTotal(
|
||||
order
|
||||
)
|
||||
order.gift_card_total = giftCardBreakdown.total
|
||||
order.gift_card_tax_total = giftCardBreakdown.tax_total
|
||||
break
|
||||
}
|
||||
case "discount_total": {
|
||||
order.discount_total = this.totalsService_.getDiscountTotal(order)
|
||||
order.discount_total = await this.totalsService_.getDiscountTotal(
|
||||
order
|
||||
)
|
||||
break
|
||||
}
|
||||
case "tax_total": {
|
||||
@@ -1447,7 +1463,7 @@ class OrderService extends TransactionBaseService {
|
||||
break
|
||||
}
|
||||
case "subtotal": {
|
||||
order.subtotal = this.totalsService_.getSubtotal(order)
|
||||
order.subtotal = await this.totalsService_.getSubtotal(order)
|
||||
break
|
||||
}
|
||||
case "total": {
|
||||
@@ -1471,36 +1487,48 @@ class OrderService extends TransactionBaseService {
|
||||
break
|
||||
}
|
||||
case "items.refundable": {
|
||||
order.items = order.items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
})) as LineItem[]
|
||||
const items: LineItem[] = []
|
||||
for (const item of order.items) {
|
||||
items.push({
|
||||
...item,
|
||||
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||
...item,
|
||||
quantity: item.quantity - (item.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
} as LineItem)
|
||||
}
|
||||
order.items = items
|
||||
break
|
||||
}
|
||||
case "swaps.additional_items.refundable": {
|
||||
for (const s of order.swaps) {
|
||||
s.additional_items = s.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
})) as LineItem[]
|
||||
const items: LineItem[] = []
|
||||
for (const item of s.additional_items) {
|
||||
items.push({
|
||||
...item,
|
||||
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||
...item,
|
||||
quantity: item.quantity - (item.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
} as LineItem)
|
||||
}
|
||||
s.additional_items = items
|
||||
}
|
||||
break
|
||||
}
|
||||
case "claims.additional_items.refundable": {
|
||||
for (const c of order.claims) {
|
||||
c.additional_items = c.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
})) as LineItem[]
|
||||
const items: LineItem[] = []
|
||||
for (const item of c.additional_items) {
|
||||
items.push({
|
||||
...item,
|
||||
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||
...item,
|
||||
quantity: item.quantity - (item.returned_quantity || 0),
|
||||
} as LineItem),
|
||||
} as LineItem)
|
||||
}
|
||||
c.additional_items = items
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, FindOperator } from "typeorm"
|
||||
import { DeepPartial, EntityManager, FindOperator } from "typeorm"
|
||||
import { CustomerGroupService } from "."
|
||||
import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
@@ -24,6 +24,8 @@ import { FilterableProductProps } from "../types/product"
|
||||
import ProductVariantService from "./product-variant"
|
||||
import { FilterableProductVariantProps } from "../types/product-variant"
|
||||
import { ProductVariantRepository } from "../repositories/product-variant"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
type PriceListConstructorProps = {
|
||||
manager: EntityManager
|
||||
@@ -34,6 +36,7 @@ type PriceListConstructorProps = {
|
||||
priceListRepository: typeof PriceListRepository
|
||||
moneyAmountRepository: typeof MoneyAmountRepository
|
||||
productVariantRepository: typeof ProductVariantRepository
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +54,7 @@ class PriceListService extends TransactionBaseService {
|
||||
protected readonly priceListRepo_: typeof PriceListRepository
|
||||
protected readonly moneyAmountRepo_: typeof MoneyAmountRepository
|
||||
protected readonly productVariantRepo_: typeof ProductVariantRepository
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -61,6 +65,7 @@ class PriceListService extends TransactionBaseService {
|
||||
priceListRepository,
|
||||
moneyAmountRepository,
|
||||
productVariantRepository,
|
||||
featureFlagRouter,
|
||||
}: PriceListConstructorProps) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -73,6 +78,7 @@ class PriceListService extends TransactionBaseService {
|
||||
this.priceListRepo_ = priceListRepository
|
||||
this.moneyAmountRepo_ = moneyAmountRepository
|
||||
this.productVariantRepo_ = productVariantRepository
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,18 +108,34 @@ class PriceListService extends TransactionBaseService {
|
||||
|
||||
/**
|
||||
* Creates a Price List
|
||||
* @param {CreatePriceListInput} priceListObject - the Price List to create
|
||||
* @return {Promise<PriceList>} created Price List
|
||||
* @param priceListObject - the Price List to create
|
||||
* @return created Price List
|
||||
*/
|
||||
async create(priceListObject: CreatePriceListInput): Promise<PriceList> {
|
||||
async create(
|
||||
priceListObject: CreatePriceListInput
|
||||
): Promise<PriceList | never> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||
|
||||
const { prices, customer_groups, ...rest } = priceListObject
|
||||
const { prices, customer_groups, includes_tax, ...rest } = priceListObject
|
||||
|
||||
try {
|
||||
const entity = priceListRepo.create(rest)
|
||||
const rawPriceList: DeepPartial<PriceList> = {
|
||||
...rest,
|
||||
}
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof includes_tax !== "undefined") {
|
||||
rawPriceList.includes_tax = includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
const entity = priceListRepo.create(rawPriceList)
|
||||
|
||||
const priceList = await priceListRepo.save(entity)
|
||||
|
||||
@@ -125,11 +147,9 @@ class PriceListService extends TransactionBaseService {
|
||||
await this.upsertCustomerGroups_(priceList.id, customer_groups)
|
||||
}
|
||||
|
||||
const result = await this.retrieve(priceList.id, {
|
||||
return await this.retrieve(priceList.id, {
|
||||
relations: ["prices", "customer_groups"],
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
throw formatException(error)
|
||||
}
|
||||
@@ -149,7 +169,17 @@ class PriceListService extends TransactionBaseService {
|
||||
|
||||
const priceList = await this.retrieve(id, { select: ["id"] })
|
||||
|
||||
const { prices, customer_groups, ...rest } = update
|
||||
const { prices, customer_groups, includes_tax, ...rest } = update
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof includes_tax !== "undefined") {
|
||||
priceList.includes_tax = includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
if (prices) {
|
||||
const prices_ = await this.addCurrencyFromRegion(prices)
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ProductVariantService, RegionService, TaxProviderService } from "."
|
||||
import { Product, ProductVariant, ShippingOption } from "../models"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
import {
|
||||
ProductVariantPricing,
|
||||
TaxedPricing,
|
||||
PricingContext,
|
||||
PricedProduct,
|
||||
PricedShippingOption,
|
||||
PricedVariant,
|
||||
} from "../types/pricing"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import {
|
||||
IPriceSelectionStrategy,
|
||||
PriceSelectionContext,
|
||||
} from "../interfaces/price-selection-strategy"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { Product, ProductVariant, ShippingOption } from "../models"
|
||||
import {
|
||||
PricedProduct,
|
||||
PricedShippingOption,
|
||||
PricedVariant,
|
||||
PricingContext,
|
||||
ProductVariantPricing,
|
||||
TaxedPricing,
|
||||
} from "../types/pricing"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
import { calculatePriceTaxAmount } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -23,6 +26,7 @@ type InjectedDependencies = {
|
||||
taxProviderService: TaxProviderService
|
||||
regionService: RegionService
|
||||
priceSelectionStrategy: IPriceSelectionStrategy
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,6 +40,7 @@ class PricingService extends TransactionBaseService {
|
||||
protected readonly taxProviderService: TaxProviderService
|
||||
protected readonly priceSelectionStrategy: IPriceSelectionStrategy
|
||||
protected readonly productVariantService: ProductVariantService
|
||||
protected readonly featureFlagRouter: FlagRouter
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -43,6 +48,7 @@ class PricingService extends TransactionBaseService {
|
||||
taxProviderService,
|
||||
regionService,
|
||||
priceSelectionStrategy,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -52,6 +58,7 @@ class PricingService extends TransactionBaseService {
|
||||
this.taxProviderService = taxProviderService
|
||||
this.priceSelectionStrategy = priceSelectionStrategy
|
||||
this.productVariantService = productVariantService
|
||||
this.featureFlagRouter = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,17 +122,43 @@ class PricingService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
if (variantPricing.calculated_price !== null) {
|
||||
const taxAmount = Math.round(variantPricing.calculated_price * rate)
|
||||
taxedPricing.calculated_tax = taxAmount
|
||||
const includesTax = !!(
|
||||
this.featureFlagRouter.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && variantPricing.calculated_price_includes_tax
|
||||
)
|
||||
taxedPricing.calculated_tax = Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: variantPricing.calculated_price,
|
||||
taxRate: rate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
|
||||
taxedPricing.calculated_price_incl_tax =
|
||||
variantPricing.calculated_price + taxAmount
|
||||
variantPricing.calculated_price_includes_tax
|
||||
? variantPricing.calculated_price
|
||||
: variantPricing.calculated_price + taxedPricing.calculated_tax
|
||||
}
|
||||
|
||||
if (variantPricing.original_price !== null) {
|
||||
const taxAmount = Math.round(variantPricing.original_price * rate)
|
||||
taxedPricing.original_tax = taxAmount
|
||||
const includesTax = !!(
|
||||
this.featureFlagRouter.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && variantPricing.original_price_includes_tax
|
||||
)
|
||||
taxedPricing.original_tax = Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: variantPricing.original_price,
|
||||
taxRate: rate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
|
||||
taxedPricing.original_price_incl_tax =
|
||||
variantPricing.original_price + taxAmount
|
||||
variantPricing.original_price_includes_tax
|
||||
? variantPricing.original_price
|
||||
: variantPricing.original_price + taxedPricing.original_tax
|
||||
}
|
||||
|
||||
return taxedPricing
|
||||
@@ -137,6 +170,9 @@ class PricingService extends TransactionBaseService {
|
||||
context: PricingContext
|
||||
): Promise<ProductVariantPricing> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
context.price_selection.tax_rates = taxRates
|
||||
|
||||
const pricing = await this.priceSelectionStrategy
|
||||
.withTransaction(transactionManager)
|
||||
.calculateVariantPrice(variantId, context.price_selection)
|
||||
@@ -146,6 +182,8 @@ class PricingService extends TransactionBaseService {
|
||||
original_price: pricing.originalPrice,
|
||||
calculated_price: pricing.calculatedPrice,
|
||||
calculated_price_type: pricing.calculatedPriceType,
|
||||
original_price_includes_tax: pricing.originalPriceIncludesTax,
|
||||
calculated_price_includes_tax: pricing.calculatedPriceIncludesTax,
|
||||
original_price_incl_tax: null,
|
||||
calculated_price_incl_tax: null,
|
||||
original_tax: null,
|
||||
@@ -418,14 +456,29 @@ class PricingService extends TransactionBaseService {
|
||||
},
|
||||
0
|
||||
)
|
||||
const tax = Math.round(price * rate)
|
||||
const total = price + tax
|
||||
|
||||
return {
|
||||
const includesTax =
|
||||
this.featureFlagRouter.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && shippingOption.includes_tax
|
||||
|
||||
const taxAmount = Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
taxRate: rate,
|
||||
price,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
const totalInclTax = includesTax ? price : price + taxAmount
|
||||
|
||||
const result: PricedShippingOption = {
|
||||
...shippingOption,
|
||||
price_incl_tax: total,
|
||||
price_incl_tax: totalInclTax,
|
||||
tax_rates: shippingOptionRates,
|
||||
tax_amount: taxAmount,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,22 +2,24 @@ import { DeepPartial, EntityManager } from "typeorm"
|
||||
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
import StoreService from "./store"
|
||||
import EventBusService from "./event-bus"
|
||||
import { countries } from "../utils/countries"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { RegionRepository } from "../repositories/region"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { Country, Currency, Region } from "../models"
|
||||
import { CountryRepository } from "../repositories/country"
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import { PaymentProviderRepository } from "../repositories/payment-provider"
|
||||
import { FulfillmentProviderRepository } from "../repositories/fulfillment-provider"
|
||||
import { PaymentProviderRepository } from "../repositories/payment-provider"
|
||||
import { RegionRepository } from "../repositories/region"
|
||||
import { TaxProviderRepository } from "../repositories/tax-provider"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import { Country, Currency, Region } from "../models"
|
||||
import { FindConfig, Selector } from "../types/common"
|
||||
import { CreateRegionInput, UpdateRegionInput } from "../types/region"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { countries } from "../utils/countries"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import EventBusService from "./event-bus"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import { PaymentProviderService } from "./index"
|
||||
import StoreService from "./store"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -25,6 +27,7 @@ type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
paymentProviderService: PaymentProviderService
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
featureFlagRouter: FlagRouter
|
||||
|
||||
regionRepository: typeof RegionRepository
|
||||
countryRepository: typeof CountryRepository
|
||||
@@ -47,6 +50,7 @@ class RegionService extends TransactionBaseService {
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
protected featureFlagRouter_: FlagRouter
|
||||
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly storeService_: StoreService
|
||||
@@ -71,6 +75,7 @@ class RegionService extends TransactionBaseService {
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
fulfillmentProviderService,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
super({
|
||||
manager,
|
||||
@@ -84,6 +89,7 @@ class RegionService extends TransactionBaseService {
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
fulfillmentProviderService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
this.manager_ = manager
|
||||
@@ -97,6 +103,8 @@ class RegionService extends TransactionBaseService {
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.taxProviderRepository_ = taxProviderRepository
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +123,20 @@ class RegionService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
const regionObject = { ...data } as DeepPartial<Region>
|
||||
const { metadata, currency_code, ...toValidate } = data
|
||||
const { metadata, currency_code, includes_tax, ...toValidate } = data
|
||||
|
||||
const validated = await this.validateFields(toValidate)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof includes_tax !== "undefined") {
|
||||
regionObject.includes_tax = includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
if (currency_code) {
|
||||
// will throw if currency is not added to store currencies
|
||||
await this.validateCurrency(currency_code)
|
||||
@@ -179,10 +197,20 @@ class RegionService extends TransactionBaseService {
|
||||
|
||||
const region = await this.retrieve(regionId)
|
||||
|
||||
const { metadata, currency_code, ...toValidate } = update
|
||||
const { metadata, currency_code, includes_tax, ...toValidate } = update
|
||||
|
||||
const validated = await this.validateFields(toValidate, region.id)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof includes_tax !== "undefined") {
|
||||
region.includes_tax = includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
if (currency_code) {
|
||||
// will throw if currency is not added to store currencies
|
||||
await this.validateCurrency(currency_code)
|
||||
|
||||
@@ -415,7 +415,7 @@ class ReturnService extends TransactionBaseService {
|
||||
}
|
||||
} else {
|
||||
// Merchant hasn't specified refund amount so we calculate it
|
||||
toRefund = this.totalsService_.getRefundTotal(order, returnLines)
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, returnLines)
|
||||
}
|
||||
|
||||
const method = data.shipping_method
|
||||
@@ -469,7 +469,7 @@ class ReturnService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
const calculationContext =
|
||||
this.totalsService_.getCalculationContext(order)
|
||||
await this.totalsService_.getCalculationContext(order)
|
||||
|
||||
const taxLines = await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
|
||||
@@ -22,6 +22,18 @@ import {
|
||||
import { buildQuery, isDefined, setMetadata } from "../utils"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import RegionService from "./region"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
regionService: RegionService
|
||||
shippingOptionRequirementRepository: typeof ShippingOptionRequirementRepository
|
||||
shippingOptionRepository: typeof ShippingOptionRepository
|
||||
shippingMethodRepository: typeof ShippingMethodRepository
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate profiles.
|
||||
@@ -32,6 +44,7 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
protected readonly requirementRepository_: typeof ShippingOptionRequirementRepository
|
||||
protected readonly optionRepository_: typeof ShippingOptionRepository
|
||||
protected readonly methodRepository_: typeof ShippingMethodRepository
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
@@ -43,7 +56,8 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
shippingMethodRepository,
|
||||
fulfillmentProviderService,
|
||||
regionService,
|
||||
}) {
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
@@ -53,6 +67,7 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
this.requirementRepository_ = shippingOptionRequirementRepository
|
||||
this.providerService_ = fulfillmentProviderService
|
||||
this.regionService_ = regionService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,6 +300,16 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
price: methodPrice,
|
||||
}
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof option.includes_tax !== "undefined") {
|
||||
toCreate.includes_tax = option.includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
if (config.order) {
|
||||
toCreate.order_id = config.order.id
|
||||
}
|
||||
@@ -313,10 +338,10 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
|
||||
const created = await methodRepo.save(method)
|
||||
|
||||
return methodRepo.findOne({
|
||||
return (await methodRepo.findOne({
|
||||
where: { id: created.id },
|
||||
relations: ["shipping_option"],
|
||||
}) as unknown as ShippingMethod
|
||||
})) as ShippingMethod
|
||||
})
|
||||
}
|
||||
|
||||
@@ -405,6 +430,16 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
option.amount =
|
||||
data.price_type === "calculated" ? null : data.amount ?? null
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof data.includes_tax !== "undefined") {
|
||||
option.includes_tax = data.includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = await this.providerService_.validateOption(option)
|
||||
|
||||
if (!isValid) {
|
||||
@@ -584,6 +619,16 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
option.admin_only = update.admin_only
|
||||
}
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
if (typeof update.includes_tax !== "undefined") {
|
||||
option.includes_tax = update.includes_tax
|
||||
}
|
||||
}
|
||||
|
||||
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
||||
return await optionRepo.save(option)
|
||||
})
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { Store } from "../models"
|
||||
import { Currency, Store } from "../models"
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import { StoreRepository } from "../repositories/store"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { UpdateStoreInput } from "../types/store"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { currencies, Currency } from "../utils/currencies"
|
||||
import { currencies } from "../utils/currencies"
|
||||
import EventBusService from "./event-bus"
|
||||
|
||||
type InjectedDependencies = {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import {
|
||||
ITaxCalculationStrategy,
|
||||
TaxCalculationContext,
|
||||
TransactionBaseService,
|
||||
} from "../interfaces"
|
||||
import { Cart } from "../models/cart"
|
||||
import { Discount } from "../models/discount"
|
||||
import { DiscountRuleType } from "../models/discount-rule"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { Order } from "../models/order"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import {
|
||||
Cart,
|
||||
Discount,
|
||||
DiscountRuleType,
|
||||
LineItem,
|
||||
LineItemTaxLine,
|
||||
Order,
|
||||
ShippingMethod,
|
||||
ShippingMethodTaxLine,
|
||||
} from "../models"
|
||||
import { isCart } from "../types/cart"
|
||||
import { isOrder } from "../types/orders"
|
||||
import {
|
||||
@@ -23,12 +24,16 @@ import {
|
||||
} from "../types/totals"
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { isDefined } from "../utils"
|
||||
|
||||
import { calculatePriceTaxAmount, isDefined } from "../utils"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
|
||||
type ShippingMethodTotals = {
|
||||
price: number
|
||||
tax_total: number
|
||||
total: number
|
||||
subtotal: number
|
||||
original_total: number
|
||||
original_tax_total: number
|
||||
tax_lines: ShippingMethodTaxLine[]
|
||||
@@ -55,6 +60,7 @@ type LineItemTotals = {
|
||||
type LineItemTotalsOptions = {
|
||||
include_tax?: boolean
|
||||
use_tax_lines?: boolean
|
||||
exclude_gift_cards?: boolean
|
||||
}
|
||||
|
||||
type GetLineItemTotalOptions = {
|
||||
@@ -67,6 +73,7 @@ type TotalsServiceProps = {
|
||||
taxProviderService: TaxProviderService
|
||||
taxCalculationStrategy: ITaxCalculationStrategy
|
||||
manager: EntityManager
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
type GetTotalsOptions = {
|
||||
@@ -94,24 +101,29 @@ class TotalsService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager
|
||||
|
||||
private taxProviderService_: TaxProviderService
|
||||
private taxCalculationStrategy_: ITaxCalculationStrategy
|
||||
protected readonly taxProviderService_: TaxProviderService
|
||||
protected readonly taxCalculationStrategy_: ITaxCalculationStrategy
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
taxProviderService,
|
||||
taxCalculationStrategy,
|
||||
featureFlagRouter,
|
||||
}: TotalsServiceProps) {
|
||||
super({
|
||||
taxProviderService,
|
||||
taxCalculationStrategy,
|
||||
manager,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
this.manager_ = manager
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.taxCalculationStrategy_ = taxCalculationStrategy
|
||||
|
||||
this.manager_ = manager
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,14 +136,14 @@ class TotalsService extends TransactionBaseService {
|
||||
cartOrOrder: Cart | Order,
|
||||
options: GetTotalsOptions = {}
|
||||
): Promise<number> {
|
||||
const subtotal = this.getSubtotal(cartOrOrder)
|
||||
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||
const taxTotal =
|
||||
(await this.getTaxTotal(cartOrOrder, options.force_taxes)) || 0
|
||||
const discountTotal = this.getDiscountTotal(cartOrOrder)
|
||||
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||
const giftCardTotal = options.exclude_gift_cards
|
||||
? { total: 0 }
|
||||
: this.getGiftCardTotal(cartOrOrder)
|
||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
||||
: await this.getGiftCardTotal(cartOrOrder)
|
||||
const shippingTotal = await this.getShippingTotal(cartOrOrder)
|
||||
|
||||
return (
|
||||
subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal.total
|
||||
@@ -182,7 +194,7 @@ class TotalsService extends TransactionBaseService {
|
||||
cartOrOrder: Cart | Order,
|
||||
opts: GetShippingMethodTotalsOptions = {}
|
||||
): Promise<ShippingMethodTotals> {
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
||||
const calculationContext = await this.getCalculationContext(cartOrOrder, {
|
||||
exclude_shipping: true,
|
||||
})
|
||||
calculationContext.shipping_methods = [shippingMethod]
|
||||
@@ -191,64 +203,70 @@ class TotalsService extends TransactionBaseService {
|
||||
price: shippingMethod.price,
|
||||
original_total: shippingMethod.price,
|
||||
total: shippingMethod.price,
|
||||
subtotal: shippingMethod.price,
|
||||
original_tax_total: 0,
|
||||
tax_total: 0,
|
||||
tax_lines: shippingMethod.tax_lines || [],
|
||||
}
|
||||
|
||||
if (opts.include_tax) {
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate !== null) {
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate != null) {
|
||||
totals.original_tax_total = Math.round(
|
||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
||||
totals.price * (cartOrOrder.tax_rate / 100)
|
||||
)
|
||||
totals.tax_total = Math.round(
|
||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
||||
totals.price * (cartOrOrder.tax_rate / 100)
|
||||
)
|
||||
} else {
|
||||
let taxLines: ShippingMethodTaxLine[]
|
||||
if (opts.use_tax_lines || isOrder(cartOrOrder)) {
|
||||
if (typeof shippingMethod.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Lines must be joined on shipping method to calculate taxes"
|
||||
)
|
||||
} else if (totals.tax_lines.length === 0) {
|
||||
const orderLines = await this.taxProviderService_
|
||||
.withTransaction(this.manager_)
|
||||
.getTaxLines(cartOrOrder.items, calculationContext)
|
||||
|
||||
totals.tax_lines = orderLines.filter((ol) => {
|
||||
if ("shipping_method_id" in ol) {
|
||||
return ol.shipping_method_id === shippingMethod.id
|
||||
}
|
||||
return false
|
||||
}) as ShippingMethodTaxLine[]
|
||||
|
||||
taxLines = shippingMethod.tax_lines
|
||||
} else {
|
||||
const orderLines = await this.taxProviderService_
|
||||
.withTransaction(this.manager_)
|
||||
.getTaxLines(cartOrOrder.items, calculationContext)
|
||||
|
||||
taxLines = orderLines.filter((ol) => {
|
||||
if ("shipping_method_id" in ol) {
|
||||
return ol.shipping_method_id === shippingMethod.id
|
||||
}
|
||||
return false
|
||||
}) as ShippingMethodTaxLine[]
|
||||
if (totals.tax_lines.length === 0 && isOrder(cartOrOrder)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Lines must be joined on shipping method to calculate taxes"
|
||||
)
|
||||
}
|
||||
totals.tax_lines = taxLines
|
||||
}
|
||||
|
||||
if (totals.tax_lines.length > 0) {
|
||||
totals.original_tax_total =
|
||||
await this.taxCalculationStrategy_.calculate(
|
||||
[],
|
||||
totals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
const includesTax =
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && shippingMethod.includes_tax
|
||||
|
||||
totals.original_tax_total = await this.taxCalculationStrategy_.calculate(
|
||||
[],
|
||||
totals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
totals.tax_total = totals.original_tax_total
|
||||
|
||||
totals.original_total += totals.original_tax_total
|
||||
totals.total += totals.tax_total
|
||||
if (includesTax) {
|
||||
totals.subtotal -= totals.tax_total
|
||||
} else {
|
||||
totals.original_total += totals.original_tax_total
|
||||
totals.total += totals.tax_total
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cartOrOrder.discounts) {
|
||||
if (cartOrOrder.discounts.some((d) => d.rule.type === "free_shipping")) {
|
||||
totals.total = 0
|
||||
totals.tax_total = 0
|
||||
}
|
||||
const hasFreeShipping = cartOrOrder.discounts?.some(
|
||||
(d) => d.rule.type === DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
|
||||
if (hasFreeShipping) {
|
||||
totals.total = 0
|
||||
totals.subtotal = 0
|
||||
totals.tax_total = 0
|
||||
}
|
||||
|
||||
return totals
|
||||
@@ -260,21 +278,33 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param opts - options
|
||||
* @return the calculated subtotal
|
||||
*/
|
||||
getSubtotal(cartOrOrder: Cart | Order, opts: SubtotalOptions = {}): number {
|
||||
async getSubtotal(
|
||||
cartOrOrder: Cart | Order,
|
||||
opts: SubtotalOptions = {}
|
||||
): Promise<number> {
|
||||
let subtotal = 0
|
||||
if (!cartOrOrder.items) {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
cartOrOrder.items.map((item) => {
|
||||
const getLineItemSubtotal = async (item: LineItem): Promise<number> => {
|
||||
const totals = await this.getLineItemTotals(item, cartOrOrder, {
|
||||
include_tax: true,
|
||||
exclude_gift_cards: true,
|
||||
})
|
||||
return totals.subtotal
|
||||
}
|
||||
|
||||
for (const item of cartOrOrder.items) {
|
||||
if (opts.excludeNonDiscounts) {
|
||||
if (item.allow_discounts) {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
subtotal += await getLineItemSubtotal(item)
|
||||
}
|
||||
} else {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
continue
|
||||
}
|
||||
})
|
||||
|
||||
subtotal += await getLineItemSubtotal(item)
|
||||
}
|
||||
|
||||
return this.rounded(subtotal)
|
||||
}
|
||||
@@ -284,11 +314,23 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param cartOrOrder - cart or order to calculate subtotal for
|
||||
* @return shipping total
|
||||
*/
|
||||
getShippingTotal(cartOrOrder: Cart | Order): number {
|
||||
async getShippingTotal(cartOrOrder: Cart | Order): Promise<number> {
|
||||
const { shipping_methods } = cartOrOrder
|
||||
return shipping_methods.reduce((acc, next) => {
|
||||
return acc + next.price
|
||||
}, 0)
|
||||
|
||||
let total = 0
|
||||
for (const shippingMethod of shipping_methods) {
|
||||
const totals = await this.getShippingMethodTotals(
|
||||
shippingMethod,
|
||||
cartOrOrder,
|
||||
{
|
||||
include_tax: true,
|
||||
}
|
||||
)
|
||||
|
||||
total += totals.subtotal
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,8 +353,8 @@ class TotalsService extends TransactionBaseService {
|
||||
return null
|
||||
}
|
||||
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder)
|
||||
const giftCardTotal = this.getGiftCardTotal(cartOrOrder)
|
||||
const calculationContext = await this.getCalculationContext(cartOrOrder)
|
||||
const giftCardTotal = await this.getGiftCardTotal(cartOrOrder)
|
||||
|
||||
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[]
|
||||
if (isOrder(cartOrOrder)) {
|
||||
@@ -335,9 +377,9 @@ class TotalsService extends TransactionBaseService {
|
||||
|
||||
taxLines = taxLines.concat(shippingTaxLines)
|
||||
} else {
|
||||
const subtotal = this.getSubtotal(cartOrOrder)
|
||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
||||
const discountTotal = this.getDiscountTotal(cartOrOrder)
|
||||
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||
const shippingTotal = await this.getShippingTotal(cartOrOrder)
|
||||
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||
return this.rounded(
|
||||
(subtotal - discountTotal - giftCardTotal.total + shippingTotal) *
|
||||
(cartOrOrder.tax_rate / 100)
|
||||
@@ -388,17 +430,17 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param options - controls what should be included in allocation map
|
||||
* @return the allocation map for the line items in the cart or order.
|
||||
*/
|
||||
getAllocationMap(
|
||||
async getAllocationMap(
|
||||
orderOrCart: Cart | Order,
|
||||
options: AllocationMapOptions = {}
|
||||
): LineAllocationsMap {
|
||||
): Promise<LineAllocationsMap> {
|
||||
const allocationMap: LineAllocationsMap = {}
|
||||
|
||||
if (!options.exclude_discounts) {
|
||||
let lineDiscounts: LineDiscountAmount[] = []
|
||||
|
||||
const discount = orderOrCart.discounts.find(
|
||||
({ rule }) => rule.type !== "free_shipping"
|
||||
const discount = orderOrCart.discounts?.find(
|
||||
({ rule }) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
if (discount) {
|
||||
lineDiscounts = this.getLineDiscounts(orderOrCart, discount)
|
||||
@@ -424,8 +466,8 @@ class TotalsService extends TransactionBaseService {
|
||||
if (!options.exclude_gift_cards) {
|
||||
let lineGiftCards: LineDiscountAmount[] = []
|
||||
if (orderOrCart.gift_cards && orderOrCart.gift_cards.length) {
|
||||
const subtotal = this.getSubtotal(orderOrCart)
|
||||
const giftCardTotal = this.getGiftCardTotal(orderOrCart)
|
||||
const subtotal = await this.getSubtotal(orderOrCart)
|
||||
const giftCardTotal = await this.getGiftCardTotal(orderOrCart)
|
||||
|
||||
// If the fixed discount exceeds the subtotal we should
|
||||
// calculate a 100% discount
|
||||
@@ -480,20 +522,38 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param lineItem - the line item to calculate the refund amount for.
|
||||
* @return the line item refund amount.
|
||||
*/
|
||||
getLineItemRefund(order: Order, lineItem: LineItem): number {
|
||||
const allocationMap = this.getAllocationMap(order)
|
||||
async getLineItemRefund(order: Order, lineItem: LineItem): Promise<number> {
|
||||
const allocationMap = await this.getAllocationMap(order)
|
||||
|
||||
const includesTax =
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && lineItem.includes_tax
|
||||
|
||||
const discountAmount =
|
||||
(allocationMap[lineItem.id]?.discount?.unit_amount || 0) *
|
||||
lineItem.quantity
|
||||
|
||||
const lineSubtotal =
|
||||
lineItem.unit_price * lineItem.quantity - discountAmount
|
||||
let lineSubtotal = lineItem.unit_price * lineItem.quantity - discountAmount
|
||||
|
||||
/*
|
||||
* Used for backcompat with old tax system
|
||||
*/
|
||||
if (order.tax_rate !== null) {
|
||||
const taxAmountIncludedInPrice = !includesTax
|
||||
? 0
|
||||
: Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: lineItem.unit_price,
|
||||
taxRate: order.tax_rate / 100,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
|
||||
lineSubtotal =
|
||||
(lineItem.unit_price - taxAmountIncludedInPrice) * lineItem.quantity -
|
||||
discountAmount
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
return this.rounded(lineSubtotal * (1 + taxRate))
|
||||
}
|
||||
@@ -508,6 +568,23 @@ class TotalsService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const taxRate = lineItem.tax_lines.reduce((acc, next) => {
|
||||
return acc + next.rate / 100
|
||||
}, 0)
|
||||
const taxAmountIncludedInPrice = !includesTax
|
||||
? 0
|
||||
: Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: lineItem.unit_price,
|
||||
taxRate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
|
||||
lineSubtotal =
|
||||
(lineItem.unit_price - taxAmountIncludedInPrice) * lineItem.quantity -
|
||||
discountAmount
|
||||
|
||||
const taxTotal = lineItem.tax_lines.reduce((acc, next) => {
|
||||
const taxRate = next.rate / 100
|
||||
return acc + this.rounded(lineSubtotal * taxRate)
|
||||
@@ -524,7 +601,7 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param lineItems - the line items to calculate refund total for
|
||||
* @return the calculated subtotal
|
||||
*/
|
||||
getRefundTotal(order: Order, lineItems: LineItem[]): number {
|
||||
async getRefundTotal(order: Order, lineItems: LineItem[]): Promise<number> {
|
||||
let itemIds = order.items.map((i) => i.id)
|
||||
|
||||
// in case we swap a swap, we need to include swap items
|
||||
@@ -542,16 +619,18 @@ class TotalsService extends TransactionBaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const refunds = lineItems.map((i) => {
|
||||
if (!itemIds.includes(i.id)) {
|
||||
const refunds: number[] = []
|
||||
for (const item of lineItems) {
|
||||
if (!itemIds.includes(item.id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Line item does not exist on order"
|
||||
)
|
||||
}
|
||||
|
||||
return this.getLineItemRefund(order, i)
|
||||
})
|
||||
const refund = await this.getLineItemRefund(order, item)
|
||||
refunds.push(refund)
|
||||
}
|
||||
|
||||
return this.rounded(refunds.reduce((acc, next) => acc + next, 0))
|
||||
}
|
||||
@@ -624,7 +703,7 @@ class TotalsService extends TransactionBaseService {
|
||||
lineItem: LineItem,
|
||||
discount: Discount
|
||||
): number {
|
||||
const matchingDiscount = lineItem.adjustments.find(
|
||||
const matchingDiscount = lineItem.adjustments?.find(
|
||||
(adjustment) => adjustment.discount_id === discount.id
|
||||
)
|
||||
|
||||
@@ -709,13 +788,24 @@ class TotalsService extends TransactionBaseService {
|
||||
cartOrOrder: Cart | Order,
|
||||
options: LineItemTotalsOptions = {}
|
||||
): Promise<LineItemTotals> {
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
||||
const calculationContext = await this.getCalculationContext(cartOrOrder, {
|
||||
exclude_shipping: true,
|
||||
exclude_gift_cards: options.exclude_gift_cards,
|
||||
})
|
||||
const lineItemAllocation =
|
||||
calculationContext.allocation_map[lineItem.id] || {}
|
||||
|
||||
const subtotal = lineItem.unit_price * lineItem.quantity
|
||||
let subtotal = lineItem.unit_price * lineItem.quantity
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) &&
|
||||
lineItem.includes_tax &&
|
||||
options.include_tax
|
||||
) {
|
||||
subtotal = 0 // in that case we need to know the tax rate to compute it later
|
||||
}
|
||||
|
||||
const gift_card_total = lineItemAllocation.gift_card?.amount || 0
|
||||
const discount_total =
|
||||
(lineItemAllocation.discount?.unit_amount || 0) * lineItem.quantity
|
||||
@@ -735,14 +825,32 @@ class TotalsService extends TransactionBaseService {
|
||||
|
||||
// Tax Information
|
||||
if (options.include_tax) {
|
||||
// When we have an order with a null'ed tax rate we know that it is an
|
||||
// When we have an order with a nulled or undefined tax rate we know that it is an
|
||||
// order from the old tax system. The following is a backward compat
|
||||
// calculation.
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate !== null) {
|
||||
lineItemTotals.original_tax_total =
|
||||
subtotal * (cartOrOrder.tax_rate / 100)
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate != null) {
|
||||
const taxRate = cartOrOrder.tax_rate / 100
|
||||
|
||||
const includesTax =
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && lineItem.includes_tax
|
||||
const taxIncludedInPrice = !lineItem.includes_tax
|
||||
? 0
|
||||
: Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: lineItem.unit_price,
|
||||
taxRate: taxRate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
lineItemTotals.subtotal =
|
||||
(lineItem.unit_price - taxIncludedInPrice) * lineItem.quantity
|
||||
lineItemTotals.total = lineItemTotals.subtotal
|
||||
|
||||
lineItemTotals.original_tax_total = lineItemTotals.subtotal * taxRate
|
||||
lineItemTotals.tax_total =
|
||||
(subtotal - discount_total) * (cartOrOrder.tax_rate / 100)
|
||||
(lineItemTotals.subtotal - discount_total) * taxRate
|
||||
|
||||
lineItemTotals.total += lineItemTotals.tax_total
|
||||
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||
@@ -795,15 +903,27 @@ class TotalsService extends TransactionBaseService {
|
||||
lineItemTotals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
lineItemTotals.total += lineItemTotals.tax_total
|
||||
|
||||
calculationContext.allocation_map = {} // Don't account for discounts
|
||||
lineItemTotals.original_tax_total =
|
||||
await this.taxCalculationStrategy_.calculate(
|
||||
[lineItem],
|
||||
lineItemTotals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
lineItemTotals.original_tax_total = await this.taxCalculationStrategy_.calculate(
|
||||
[lineItem],
|
||||
lineItemTotals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) &&
|
||||
lineItem.includes_tax
|
||||
) {
|
||||
lineItemTotals.subtotal +=
|
||||
lineItem.unit_price * lineItem.quantity -
|
||||
lineItemTotals.original_tax_total
|
||||
lineItemTotals.total += lineItemTotals.subtotal
|
||||
lineItemTotals.original_total += lineItemTotals.subtotal
|
||||
}
|
||||
|
||||
lineItemTotals.total += lineItemTotals.tax_total
|
||||
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||
}
|
||||
|
||||
@@ -851,7 +971,9 @@ class TotalsService extends TransactionBaseService {
|
||||
*/
|
||||
async getGiftCardableAmount(cartOrOrder: Cart | Order): Promise<number> {
|
||||
if (cartOrOrder.region?.gift_cards_taxable) {
|
||||
return this.getSubtotal(cartOrOrder) - this.getDiscountTotal(cartOrOrder)
|
||||
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||
return subtotal - discountTotal
|
||||
}
|
||||
|
||||
return await this.getTotal(cartOrOrder, {
|
||||
@@ -864,12 +986,15 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param cartOrOrder - the cart or order to get gift card amount for
|
||||
* @return the gift card amount applied to the cart or order
|
||||
*/
|
||||
getGiftCardTotal(cartOrOrder: Cart | Order): {
|
||||
async getGiftCardTotal(
|
||||
cartOrOrder: Cart | Order
|
||||
): Promise<{
|
||||
total: number
|
||||
tax_total: number
|
||||
} {
|
||||
const giftCardable =
|
||||
this.getSubtotal(cartOrOrder) - this.getDiscountTotal(cartOrOrder)
|
||||
}> {
|
||||
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||
const giftCardable = subtotal - discountTotal
|
||||
|
||||
if ("gift_card_transactions" in cartOrOrder) {
|
||||
// gift_card_transactions only exist on orders so we can
|
||||
@@ -936,19 +1061,15 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param cartOrOrder - the cart or order to calculate discounts for
|
||||
* @return the total discounts amount
|
||||
*/
|
||||
getDiscountTotal(cartOrOrder: Cart | Order): number {
|
||||
const subtotal = this.getSubtotal(cartOrOrder, {
|
||||
async getDiscountTotal(cartOrOrder: Cart | Order): Promise<number> {
|
||||
const subtotal = await this.getSubtotal(cartOrOrder, {
|
||||
excludeNonDiscounts: true,
|
||||
})
|
||||
|
||||
if (!cartOrOrder.discounts || !cartOrOrder.discounts.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// we only support having free shipping and one other discount, so first
|
||||
// find the discount, which is not free shipping.
|
||||
const discount = cartOrOrder.discounts.find(
|
||||
({ rule }) => rule.type !== "free_shipping"
|
||||
const discount = cartOrOrder.discounts?.find(
|
||||
({ rule }) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
|
||||
if (!discount) {
|
||||
@@ -970,11 +1091,11 @@ class TotalsService extends TransactionBaseService {
|
||||
* @param options - options to gather context by
|
||||
* @return the tax calculation context
|
||||
*/
|
||||
getCalculationContext(
|
||||
async getCalculationContext(
|
||||
cartOrOrder: Cart | Order,
|
||||
options: CalculationContextOptions = {}
|
||||
): TaxCalculationContext {
|
||||
const allocationMap = this.getAllocationMap(cartOrOrder, {
|
||||
): Promise<TaxCalculationContext> {
|
||||
const allocationMap = await this.getAllocationMap(cartOrOrder, {
|
||||
exclude_gift_cards: options.exclude_gift_cards,
|
||||
exclude_discounts: options.exclude_discounts,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,249 @@
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import PriceSelectionStrategy from "../price-selection"
|
||||
|
||||
const executeTest =
|
||||
(flagValue) =>
|
||||
async (title, { variant_id, context, validate, validateException }) => {
|
||||
const mockMoneyAmountRepository = {
|
||||
findManyForVariantInRegion: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async (
|
||||
variant_id,
|
||||
region_id,
|
||||
currency_code,
|
||||
customer_id,
|
||||
useDiscountPrices
|
||||
) => {
|
||||
if (variant_id === "test-basic-variant") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (variant_id === "test-basic-variant-tax-inclusive") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
region: {
|
||||
includes_tax: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
amount: 120,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
currency: {
|
||||
includes_tax: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (variant_id === "test-basic-variant-tax-inclusive-currency") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
amount: 100,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
currency: {
|
||||
includes_tax: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (variant_id === "test-basic-variant-tax-inclusive-region") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
region: {
|
||||
includes_tax: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
amount: 100,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (variant_id === "test-basic-variant-mixed") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
region: {
|
||||
includes_tax: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
amount: 95,
|
||||
currency_code,
|
||||
price_list_id: "pl_1",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
{
|
||||
amount: 110,
|
||||
currency_code,
|
||||
price_list_id: "pl_2",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale", includes_tax: true },
|
||||
},
|
||||
{
|
||||
amount: 150,
|
||||
currency_code,
|
||||
price_list_id: "pl_3",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (customer_id === "test-customer-1") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
{
|
||||
amount: 50,
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
price_list: { type: "sale" },
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
],
|
||||
2,
|
||||
]
|
||||
}
|
||||
if (customer_id === "test-customer-2") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
{
|
||||
amount: 30,
|
||||
min_quantity: 10,
|
||||
max_quantity: 12,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
{
|
||||
amount: 20,
|
||||
min_quantity: 3,
|
||||
max_quantity: 5,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
{
|
||||
amount: 50,
|
||||
min_quantity: 5,
|
||||
max_quantity: 10,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
],
|
||||
4,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
const mockEntityManager = {
|
||||
getCustomRepository: (repotype) => mockMoneyAmountRepository,
|
||||
}
|
||||
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
tax_inclusive_pricing: flagValue,
|
||||
})
|
||||
|
||||
const selectionStrategy = new PriceSelectionStrategy({
|
||||
manager: mockEntityManager,
|
||||
moneyAmountRepository: mockMoneyAmountRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
try {
|
||||
const val = await selectionStrategy.calculateVariantPrice(
|
||||
variant_id,
|
||||
context
|
||||
)
|
||||
|
||||
validate(val, { mockMoneyAmountRepository, featureFlagRouter })
|
||||
} catch (error) {
|
||||
if (typeof validateException === "function") {
|
||||
validateException(error, { mockMoneyAmountRepository })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toTest = [
|
||||
[
|
||||
"Variant with only default price",
|
||||
@@ -9,17 +253,41 @@ const toTest = [
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
let ffFields = {}
|
||||
if (featureFlagRouter.isFeatureEnabled("tax_inclusive_pricing")) {
|
||||
ffFields = {
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)
|
||||
) {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
expect(value).toEqual({
|
||||
...ffFields,
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
calculatedPriceType: "default",
|
||||
@@ -63,16 +331,31 @@ const toTest = [
|
||||
currency_code: "dkk",
|
||||
customer_id: "test-customer-1",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
"test-customer-1",
|
||||
undefined
|
||||
)
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
if (
|
||||
featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)
|
||||
) {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
"test-customer-1",
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
"test-region",
|
||||
"dkk",
|
||||
"test-customer-1",
|
||||
undefined
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -85,8 +368,16 @@ const toTest = [
|
||||
currency_code: "dkk",
|
||||
customer_id: "test-customer-1",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
let ffFields = {}
|
||||
if (featureFlagRouter.isFeatureEnabled("tax_inclusive_pricing")) {
|
||||
ffFields = {
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
}
|
||||
}
|
||||
expect(value).toEqual({
|
||||
...ffFields,
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 50,
|
||||
calculatedPriceType: "sale",
|
||||
@@ -121,8 +412,16 @@ const toTest = [
|
||||
currency_code: "dkk",
|
||||
customer_id: "test-customer-2",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
let ffFields = {}
|
||||
if (featureFlagRouter.isFeatureEnabled("tax_inclusive_pricing")) {
|
||||
ffFields = {
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
}
|
||||
}
|
||||
expect(value).toEqual({
|
||||
...ffFields,
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
calculatedPriceType: "default",
|
||||
@@ -174,8 +473,16 @@ const toTest = [
|
||||
customer_id: "test-customer-2",
|
||||
quantity: 7,
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
let ffFields = {}
|
||||
if (featureFlagRouter.isFeatureEnabled("tax_inclusive_pricing")) {
|
||||
ffFields = {
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
}
|
||||
}
|
||||
expect(value).toEqual({
|
||||
...ffFields,
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 50,
|
||||
calculatedPriceType: "sale",
|
||||
@@ -226,8 +533,16 @@ const toTest = [
|
||||
currency_code: "dkk",
|
||||
customer_id: "test-customer-2",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
let ffFields = {}
|
||||
if (featureFlagRouter.isFeatureEnabled("tax_inclusive_pricing")) {
|
||||
ffFields = {
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
}
|
||||
}
|
||||
expect(value).toEqual({
|
||||
...ffFields,
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
calculatedPriceType: "default",
|
||||
@@ -271,128 +586,281 @@ const toTest = [
|
||||
],
|
||||
]
|
||||
|
||||
const taxInclusiveTesting = [
|
||||
[
|
||||
"Variant with tax inclusive prices",
|
||||
{
|
||||
variant_id: "test-basic-variant-tax-inclusive",
|
||||
context: {
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant-tax-inclusive",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(value).toEqual({
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
originalPriceIncludesTax: true,
|
||||
calculatedPriceIncludesTax: true,
|
||||
calculatedPriceType: "default",
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
region_id: "test-region",
|
||||
},
|
||||
{
|
||||
amount: 120,
|
||||
currency_code: "dkk",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Variant with mixed pricing tax inclusive prices currency",
|
||||
{
|
||||
variant_id: "test-basic-variant-tax-inclusive-currency",
|
||||
context: {
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
tax_rates: [{ rate: 25 }],
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant-tax-inclusive-currency",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(value).toEqual({
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: true,
|
||||
calculatedPriceType: "default",
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
region_id: "test-region",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
amount: 100,
|
||||
currency_code: "dkk",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Variant with mixed pricing tax inclusive prices region",
|
||||
{
|
||||
variant_id: "test-basic-variant-tax-inclusive-region",
|
||||
context: {
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
tax_rates: [{ rate: 25 }],
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant-tax-inclusive-region",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(value).toEqual({
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 100,
|
||||
originalPriceIncludesTax: true,
|
||||
calculatedPriceIncludesTax: true,
|
||||
calculatedPriceType: "default",
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
region_id: "test-region",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
amount: 100,
|
||||
currency_code: "dkk",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Variant with mixed tax prices (favoring tax inclusive)",
|
||||
{
|
||||
variant_id: "test-basic-variant-mixed",
|
||||
context: {
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
tax_rates: [{ rate: 25 }],
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant-mixed",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(value).toEqual({
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 110,
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: true,
|
||||
calculatedPriceType: "sale",
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
region_id: "test-region",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
amount: 95,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_1",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
{
|
||||
amount: 110,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_2",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale", includes_tax: true },
|
||||
},
|
||||
{
|
||||
amount: 150,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_3",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Variant with mixed tax price (favoring tax exclusive)",
|
||||
{
|
||||
variant_id: "test-basic-variant-mixed",
|
||||
context: {
|
||||
region_id: "test-region",
|
||||
currency_code: "dkk",
|
||||
tax_rate: 0.05,
|
||||
},
|
||||
validate: (value, { mockMoneyAmountRepository }) => {
|
||||
expect(
|
||||
mockMoneyAmountRepository.findManyForVariantInRegion
|
||||
).toHaveBeenCalledWith(
|
||||
"test-basic-variant-mixed",
|
||||
"test-region",
|
||||
"dkk",
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(value).toEqual({
|
||||
originalPrice: 100,
|
||||
calculatedPrice: 95,
|
||||
originalPriceIncludesTax: false,
|
||||
calculatedPriceIncludesTax: false,
|
||||
calculatedPriceType: "sale",
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
region_id: "test-region",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
amount: 95,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_1",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
{
|
||||
amount: 110,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_2",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale", includes_tax: true },
|
||||
},
|
||||
{
|
||||
amount: 150,
|
||||
currency_code: "dkk",
|
||||
price_list_id: "pl_3",
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
price_list: { type: "sale" },
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
describe("PriceSelectionStrategy", () => {
|
||||
describe("calculateVariantPrice", () => {
|
||||
test.each(toTest)(
|
||||
"%s",
|
||||
async (title, { variant_id, context, validate, validateException }) => {
|
||||
const mockMoneyAmountRepository = {
|
||||
findManyForVariantInRegion: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async (
|
||||
variant_id,
|
||||
region_id,
|
||||
currency_code,
|
||||
customer_id,
|
||||
useDiscountPrices
|
||||
) => {
|
||||
if (variant_id === "test-basic-variant") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
],
|
||||
1,
|
||||
]
|
||||
}
|
||||
if (customer_id === "test-customer-1") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
{
|
||||
amount: 50,
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
price_list: { type: "sale" },
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
],
|
||||
2,
|
||||
]
|
||||
}
|
||||
if (customer_id === "test-customer-2") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
region_id,
|
||||
currency_code,
|
||||
price_list_id: null,
|
||||
max_quantity: null,
|
||||
min_quantity: null,
|
||||
},
|
||||
{
|
||||
amount: 30,
|
||||
min_quantity: 10,
|
||||
max_quantity: 12,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
{
|
||||
amount: 20,
|
||||
min_quantity: 3,
|
||||
max_quantity: 5,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
{
|
||||
amount: 50,
|
||||
min_quantity: 5,
|
||||
max_quantity: 10,
|
||||
price_list: { type: "sale" },
|
||||
region_id: region_id,
|
||||
currency_code: currency_code,
|
||||
},
|
||||
],
|
||||
4,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
const mockEntityManager = {
|
||||
getCustomRepository: (repotype) => mockMoneyAmountRepository,
|
||||
}
|
||||
|
||||
const selectionStrategy = new PriceSelectionStrategy({
|
||||
manager: mockEntityManager,
|
||||
moneyAmountRepository: mockMoneyAmountRepository,
|
||||
})
|
||||
|
||||
try {
|
||||
const val = await selectionStrategy.calculateVariantPrice(
|
||||
variant_id,
|
||||
context
|
||||
)
|
||||
|
||||
validate(val, { mockMoneyAmountRepository })
|
||||
} catch (error) {
|
||||
if (typeof validateException === "function") {
|
||||
validateException(error, { mockMoneyAmountRepository })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
;[true, false].forEach((flagValue) => {
|
||||
describe(`with tax inclusive pricing ${flagValue}`, () => {
|
||||
test.each(toTest)(`%s`, executeTest(flagValue))
|
||||
})
|
||||
})
|
||||
describe("tax inclusive testing", () => {
|
||||
test.each(taxInclusiveTesting)(`%s`, executeTest(true))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,121 +1,282 @@
|
||||
import TaxCalculationStrategy from "../tax-calculation"
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
|
||||
const toTest = [
|
||||
{
|
||||
title: "calculates correctly without gift card",
|
||||
|
||||
/*
|
||||
* Subtotal = 2 * 100 = 200
|
||||
* Taxable amount = 200 - 10 = 190
|
||||
* Taxline 1 = 190 * 0.0825 = 15.675 = 16
|
||||
* Taxline 2 = 190 * 0.125 = 13.75 = 14
|
||||
* Total tax = 40
|
||||
*/
|
||||
expected: 40,
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 100,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 8.25,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 2",
|
||||
rate: 12.5,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
shipping_methods: [],
|
||||
region: {
|
||||
gift_cards_taxable: false,
|
||||
},
|
||||
allocation_map: {
|
||||
item_1: {
|
||||
discount: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
gift_card: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
[
|
||||
"calculates correctly without gift card",
|
||||
{
|
||||
/*
|
||||
* Subtotal = 2 * 100 = 200
|
||||
* Taxable amount = 200 - 10 = 190
|
||||
* Taxline 1 = 190 * 0.0825 = 15.675 = 16
|
||||
* Taxline 2 = 190 * 0.125 = 13.75 = 14
|
||||
* Total tax = 40
|
||||
*/
|
||||
expected: 40,
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 100,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 8.25,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 2",
|
||||
rate: 12.5,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
shipping_methods: [],
|
||||
region: {
|
||||
gift_cards_taxable: false,
|
||||
},
|
||||
allocation_map: {
|
||||
item_1: {
|
||||
discount: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
gift_card: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "calculates correctly with gift card",
|
||||
|
||||
/*
|
||||
* Subtotal = 2 * 100 = 200
|
||||
* Taxable amount = 200 - 10 = 180
|
||||
* Taxline 1 = 180 * 0.0825 = 15
|
||||
* Taxline 2 = 180 * 0.125 = 23
|
||||
* Total tax = 38
|
||||
*/
|
||||
expected: 40,
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 100,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 8.25,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 2",
|
||||
rate: 12.5,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
region: {
|
||||
gift_cards_taxable: true,
|
||||
},
|
||||
shipping_methods: [],
|
||||
allocation_map: {
|
||||
item_1: {
|
||||
discount: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
gift_card: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
],
|
||||
[
|
||||
"calculates correctly with gift card",
|
||||
{
|
||||
/*
|
||||
* Subtotal = 2 * 100 = 200
|
||||
* Taxable amount = 200 - 10 = 180
|
||||
* Taxline 1 = 180 * 0.0825 = 15
|
||||
* Taxline 2 = 180 * 0.125 = 23
|
||||
* Total tax = 38
|
||||
*/
|
||||
expected: 40,
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 100,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 8.25,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 2",
|
||||
rate: 12.5,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
region: {
|
||||
gift_cards_taxable: true,
|
||||
},
|
||||
shipping_methods: [],
|
||||
allocation_map: {
|
||||
item_1: {
|
||||
discount: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
gift_card: {
|
||||
amount: 10,
|
||||
unit_amount: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"calculates correctly with tax inclusive pricing",
|
||||
{
|
||||
/*
|
||||
* Subtotal = 3 * 100 = 100
|
||||
* Taxable amount = 300
|
||||
* Taxline 1 = 100 * 0.2 * 2 = 40
|
||||
* Taxline 2 = 100 * 0.2 * 1 = 20
|
||||
* Total tax = 60
|
||||
*/
|
||||
expected: 60,
|
||||
flags: { [TaxInclusivePricingFeatureFlag.key]: true },
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 120,
|
||||
quantity: 2,
|
||||
includes_tax: true,
|
||||
},
|
||||
{
|
||||
id: "item_2",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
includes_tax: false,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 20,
|
||||
},
|
||||
{
|
||||
item_id: "item_2",
|
||||
name: "Name 2",
|
||||
rate: 20,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
region: {
|
||||
gift_cards_taxable: true,
|
||||
},
|
||||
shipping_methods: [],
|
||||
allocation_map: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"calculates correctly with tax inclusive shipping",
|
||||
{
|
||||
expected: 40,
|
||||
flags: { [TaxInclusivePricingFeatureFlag.key]: true },
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 120,
|
||||
quantity: 1,
|
||||
includes_tax: true,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
shipping_method_id: "shipping_method_1",
|
||||
name: "Name 1",
|
||||
rate: 15,
|
||||
},
|
||||
{
|
||||
shipping_method_id: "shipping_method_2",
|
||||
name: "Name 2",
|
||||
rate: 5,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 20,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
region: {
|
||||
gift_cards_taxable: true,
|
||||
},
|
||||
shipping_methods: [
|
||||
{ id: "shipping_method_1", price: 115, includes_tax: true },
|
||||
{ id: "shipping_method_2", price: 105, includes_tax: true },
|
||||
],
|
||||
allocation_map: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"calculates correctly with tax inclusive pricing and shipping",
|
||||
{
|
||||
expected: 85,
|
||||
flags: { [TaxInclusivePricingFeatureFlag.key]: true },
|
||||
items: [
|
||||
{
|
||||
id: "item_1",
|
||||
unit_price: 120,
|
||||
quantity: 2,
|
||||
includes_tax: true,
|
||||
},
|
||||
{
|
||||
id: "item_2",
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
includes_tax: false,
|
||||
},
|
||||
],
|
||||
taxLines: [
|
||||
{
|
||||
shipping_method_id: "shipping_method_1",
|
||||
name: "Name 1",
|
||||
rate: 15,
|
||||
},
|
||||
{
|
||||
shipping_method_id: "shipping_method_2",
|
||||
name: "Name 2",
|
||||
rate: 10,
|
||||
},
|
||||
{
|
||||
item_id: "item_1",
|
||||
name: "Name 1",
|
||||
rate: 20,
|
||||
},
|
||||
{
|
||||
item_id: "item_2",
|
||||
name: "Name 2",
|
||||
rate: 20,
|
||||
},
|
||||
],
|
||||
context: {
|
||||
shipping_address: null,
|
||||
customer: {
|
||||
email: "test@testson.com",
|
||||
},
|
||||
region: {
|
||||
gift_cards_taxable: true,
|
||||
},
|
||||
shipping_methods: [
|
||||
{ id: "shipping_method_1", price: 115, includes_tax: true },
|
||||
{ id: "shipping_method_2", price: 100, includes_tax: false },
|
||||
],
|
||||
allocation_map: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
describe("TaxCalculationStrategy", () => {
|
||||
describe("calculate", () => {
|
||||
const calcStrat = new TaxCalculationStrategy()
|
||||
|
||||
test.each(toTest)(
|
||||
"$title",
|
||||
async ({ items, taxLines, context, expected }) => {
|
||||
"%s",
|
||||
async (title, { items, taxLines, context, expected, flags }) => {
|
||||
const featureFlagRouter = new FlagRouter(flags ?? {})
|
||||
const calcStrat = new TaxCalculationStrategy({
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
const val = await calcStrat.calculate(items, taxLines, context)
|
||||
expect(val).toEqual(expected)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import {
|
||||
AbstractPriceSelectionStrategy,
|
||||
IPriceSelectionStrategy,
|
||||
@@ -5,18 +6,22 @@ import {
|
||||
PriceSelectionResult,
|
||||
PriceType,
|
||||
} from "../interfaces/price-selection-strategy"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { isDefined } from "../utils"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import { isDefined } from "../utils/is-defined"
|
||||
|
||||
class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
private moneyAmountRepository_: typeof MoneyAmountRepository
|
||||
private featureFlagRouter_: FlagRouter
|
||||
private manager_: EntityManager
|
||||
|
||||
constructor({ manager, moneyAmountRepository }) {
|
||||
constructor({ manager, featureFlagRouter, moneyAmountRepository }) {
|
||||
super()
|
||||
this.manager_ = manager
|
||||
this.moneyAmountRepository_ = moneyAmountRepository
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
withTransaction(manager: EntityManager): IPriceSelectionStrategy {
|
||||
@@ -27,12 +32,116 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
return new PriceSelectionStrategy({
|
||||
manager: manager,
|
||||
moneyAmountRepository: this.moneyAmountRepository_,
|
||||
featureFlagRouter: this.featureFlagRouter_,
|
||||
})
|
||||
}
|
||||
|
||||
async calculateVariantPrice(
|
||||
variant_id: string,
|
||||
context: PriceSelectionContext
|
||||
): Promise<PriceSelectionResult> {
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
return this.calculateVariantPrice_new(variant_id, context)
|
||||
}
|
||||
return this.calculateVariantPrice_old(variant_id, context)
|
||||
}
|
||||
|
||||
private async calculateVariantPrice_new(
|
||||
variant_id: string,
|
||||
context: PriceSelectionContext
|
||||
): Promise<PriceSelectionResult> {
|
||||
const moneyRepo = this.manager_.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
|
||||
const [prices, count] = await moneyRepo.findManyForVariantInRegion(
|
||||
variant_id,
|
||||
context.region_id,
|
||||
context.currency_code,
|
||||
context.customer_id,
|
||||
context.include_discount_prices,
|
||||
true
|
||||
)
|
||||
|
||||
const result: PriceSelectionResult = {
|
||||
originalPrice: null,
|
||||
calculatedPrice: null,
|
||||
prices,
|
||||
originalPriceIncludesTax: null,
|
||||
calculatedPriceIncludesTax: null,
|
||||
}
|
||||
|
||||
if (!count || !context) {
|
||||
return result
|
||||
}
|
||||
|
||||
const taxRate = context.tax_rates?.reduce(
|
||||
(accRate: number, nextTaxRate: TaxServiceRate) => {
|
||||
return accRate + (nextTaxRate.rate || 0) / 100
|
||||
},
|
||||
0
|
||||
)
|
||||
|
||||
for (const ma of prices) {
|
||||
let isTaxInclusive = ma.currency?.includes_tax || false
|
||||
|
||||
if (ma.price_list?.includes_tax) {
|
||||
// PriceList specific price so use the PriceList tax setting
|
||||
isTaxInclusive = ma.price_list.includes_tax
|
||||
} else if (ma.region?.includes_tax) {
|
||||
// Region specific price so use the Region tax setting
|
||||
isTaxInclusive = ma.region.includes_tax
|
||||
}
|
||||
|
||||
delete ma.currency
|
||||
delete ma.region
|
||||
|
||||
if (
|
||||
context.region_id &&
|
||||
ma.region_id === context.region_id &&
|
||||
ma.price_list_id === null &&
|
||||
ma.min_quantity === null &&
|
||||
ma.max_quantity === null
|
||||
) {
|
||||
result.originalPriceIncludesTax = isTaxInclusive
|
||||
result.originalPrice = ma.amount
|
||||
}
|
||||
|
||||
if (
|
||||
context.currency_code &&
|
||||
ma.currency_code === context.currency_code &&
|
||||
ma.price_list_id === null &&
|
||||
ma.min_quantity === null &&
|
||||
ma.max_quantity === null &&
|
||||
result.originalPrice === null // region prices take precedence
|
||||
) {
|
||||
result.originalPriceIncludesTax = isTaxInclusive
|
||||
result.originalPrice = ma.amount
|
||||
}
|
||||
|
||||
if (
|
||||
isValidQuantity(ma, context.quantity) &&
|
||||
isValidAmount(ma.amount, result, isTaxInclusive, taxRate) &&
|
||||
((context.currency_code &&
|
||||
ma.currency_code === context.currency_code) ||
|
||||
(context.region_id && ma.region_id === context.region_id))
|
||||
) {
|
||||
result.calculatedPrice = ma.amount
|
||||
result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT
|
||||
result.calculatedPriceIncludesTax = isTaxInclusive
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async calculateVariantPrice_old(
|
||||
variant_id: string,
|
||||
context: PriceSelectionContext
|
||||
): Promise<PriceSelectionResult> {
|
||||
const moneyRepo = this.manager_.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
@@ -65,6 +174,9 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
}
|
||||
|
||||
for (const ma of prices) {
|
||||
delete ma.currency
|
||||
delete ma.region
|
||||
|
||||
if (
|
||||
context.region_id &&
|
||||
ma.region_id === context.region_id &&
|
||||
@@ -103,6 +215,31 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
const isValidAmount = (
|
||||
amount: number,
|
||||
result: PriceSelectionResult,
|
||||
isTaxInclusive: boolean,
|
||||
taxRate?: number
|
||||
): boolean => {
|
||||
if (result.calculatedPrice === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isTaxInclusive === result.calculatedPriceIncludesTax) {
|
||||
// if both or neither are tax inclusive compare equally
|
||||
return amount < result.calculatedPrice
|
||||
}
|
||||
|
||||
if (typeof taxRate !== "undefined") {
|
||||
return isTaxInclusive
|
||||
? amount < (1 + taxRate) * result.calculatedPrice
|
||||
: (1 + taxRate) * amount < result.calculatedPrice
|
||||
}
|
||||
|
||||
// if we dont have a taxrate we can't compare mixed prices
|
||||
return false
|
||||
}
|
||||
|
||||
const isValidQuantity = (price, quantity): boolean =>
|
||||
(isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) ||
|
||||
(typeof quantity === "undefined" && isValidPriceWithoutQuantity(price))
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import { TaxCalculationContext } from "../interfaces/tax-service"
|
||||
import { ITaxCalculationStrategy } from "../interfaces/tax-calculation-strategy"
|
||||
import {
|
||||
LineItem,
|
||||
LineItemTaxLine,
|
||||
ShippingMethod,
|
||||
ShippingMethodTaxLine,
|
||||
} from "../models"
|
||||
import { ITaxCalculationStrategy, TaxCalculationContext } from "../interfaces"
|
||||
import { calculatePriceTaxAmount } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({ featureFlagRouter }) {
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
async calculate(
|
||||
items: LineItem[],
|
||||
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
|
||||
@@ -33,18 +43,47 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
||||
context: TaxCalculationContext
|
||||
): number {
|
||||
let taxTotal = 0
|
||||
for (const i of items) {
|
||||
const allocations = context.allocation_map[i.id] || {}
|
||||
|
||||
let taxableAmount = i.quantity * i.unit_price
|
||||
for (const item of items) {
|
||||
const allocations = context.allocation_map[item.id] || {}
|
||||
|
||||
const filteredTaxLines = taxLines.filter((tl) => tl.item_id === item.id)
|
||||
const includesTax =
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && item.includes_tax
|
||||
|
||||
let taxableAmount
|
||||
if (includesTax) {
|
||||
const taxRate = filteredTaxLines.reduce(
|
||||
(accRate: number, nextLineItemTaxLine: LineItemTaxLine) => {
|
||||
return accRate + (nextLineItemTaxLine.rate || 0) / 100
|
||||
},
|
||||
0
|
||||
)
|
||||
const taxIncludedInPrice = Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: item.unit_price,
|
||||
taxRate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
taxableAmount = (item.unit_price - taxIncludedInPrice) * item.quantity
|
||||
} else {
|
||||
taxableAmount = item.unit_price * item.quantity
|
||||
}
|
||||
|
||||
taxableAmount -=
|
||||
((allocations.discount && allocations.discount.unit_amount) || 0) *
|
||||
i.quantity
|
||||
item.quantity
|
||||
|
||||
const lineRates = taxLines.filter((tl) => tl.item_id === i.id)
|
||||
for (const lineRate of lineRates) {
|
||||
taxTotal += Math.round(taxableAmount * (lineRate.rate / 100))
|
||||
for (const filteredTaxLine of filteredTaxLines) {
|
||||
taxTotal += Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: taxableAmount,
|
||||
taxRate: filteredTaxLine.rate / 100,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return taxTotal
|
||||
@@ -54,12 +93,19 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
||||
shipping_methods: ShippingMethod[],
|
||||
taxLines: ShippingMethodTaxLine[]
|
||||
): number {
|
||||
const taxInclusiveEnabled = this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
)
|
||||
|
||||
let taxTotal = 0
|
||||
for (const sm of shipping_methods) {
|
||||
const amount = sm.price
|
||||
const lineRates = taxLines.filter((tl) => tl.shipping_method_id === sm.id)
|
||||
for (const lineRate of lineRates) {
|
||||
taxTotal += Math.round(amount * (lineRate.rate / 100))
|
||||
taxTotal += calculatePriceTaxAmount({
|
||||
price: sm.price,
|
||||
taxRate: lineRate.rate / 100,
|
||||
includesTax: taxInclusiveEnabled && sm.includes_tax,
|
||||
})
|
||||
}
|
||||
}
|
||||
return taxTotal
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type UpdateCurrencyInput = {
|
||||
includes_tax?: boolean
|
||||
}
|
||||
@@ -126,12 +126,19 @@ export type CreatePriceListInput = {
|
||||
customer_groups?: { id: string }[]
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export type UpdatePriceListInput = Partial<
|
||||
Pick<
|
||||
PriceList,
|
||||
"name" | "description" | "starts_at" | "ends_at" | "status" | "type"
|
||||
| "name"
|
||||
| "description"
|
||||
| "starts_at"
|
||||
| "ends_at"
|
||||
| "status"
|
||||
| "type"
|
||||
| "includes_tax"
|
||||
>
|
||||
> & {
|
||||
prices?: AdminPriceListPricesUpdateReq[]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { MoneyAmount, ProductVariant, Product, ShippingOption } from "../models"
|
||||
import { TaxServiceRate } from "./tax-service"
|
||||
import { PriceSelectionContext } from "../interfaces/price-selection-strategy"
|
||||
import { MoneyAmount, Product, ProductVariant, ShippingOption } from "../models"
|
||||
import { TaxServiceRate } from "./tax-service"
|
||||
|
||||
export type ProductVariantPricing = {
|
||||
prices: MoneyAmount[]
|
||||
original_price: number | null
|
||||
calculated_price: number | null
|
||||
original_price_includes_tax?: boolean | null
|
||||
calculated_price_includes_tax?: boolean | null
|
||||
calculated_price_type?: string | null
|
||||
} & TaxedPricing
|
||||
|
||||
@@ -26,6 +28,7 @@ export type PricingContext = {
|
||||
export type ShippingOptionPricing = {
|
||||
price_incl_tax: number | null
|
||||
tax_rates: TaxServiceRate[] | null
|
||||
tax_amount: number
|
||||
}
|
||||
|
||||
export type PricedShippingOption = Partial<ShippingOption> &
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { FindConfig } from "./common"
|
||||
import { Region } from "../models"
|
||||
|
||||
export type UpdateRegionInput = {
|
||||
name?: string
|
||||
currency_code?: string
|
||||
@@ -12,6 +9,7 @@ export type UpdateRegionInput = {
|
||||
payment_providers?: string[]
|
||||
fulfillment_providers?: string[]
|
||||
countries?: string[]
|
||||
includes_tax?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -23,5 +21,6 @@ export type CreateRegionInput = {
|
||||
payment_providers: string[]
|
||||
fulfillment_providers: string[]
|
||||
countries: string[]
|
||||
includes_tax?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export type CreateShippingOptionInput = {
|
||||
profile_id: string
|
||||
provider_id: string
|
||||
data: Record<string, unknown>
|
||||
includes_tax?: boolean
|
||||
|
||||
amount?: number
|
||||
is_return?: boolean
|
||||
@@ -71,4 +72,5 @@ export type UpdateShippingOptionInput = {
|
||||
provider_id?: string
|
||||
profile_id?: string
|
||||
data?: string
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user