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:
105 files changed
+6493
-745
No files matched your search
@@ -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": "$",
|
"symbol_native": "$",
|
||||||
},
|
},
|
||||||
"default_currency_code": "usd",
|
"default_currency_code": "usd",
|
||||||
"feature_flags": Array [
|
"feature_flags": Any<Array>,
|
||||||
Object {
|
|
||||||
"key": "sales_channels",
|
|
||||||
"value": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"fulfillment_providers": Array [
|
"fulfillment_providers": Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "test-ful",
|
"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()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
File renamed without changes.
@@ -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,
|
ShippingMethod,
|
||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../../helpers/setup-server")
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
|
||||||
const orderSeeder = require("../../helpers/order-seeder")
|
const orderSeeder = require("../../../helpers/order-seeder")
|
||||||
const swapSeeder = require("../../helpers/swap-seeder")
|
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
const claimSeeder = require("../../helpers/claim-seeder")
|
const claimSeeder = require("../../../helpers/claim-seeder")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
expectPostCallToReturn,
|
expectPostCallToReturn,
|
||||||
expectAllPostCallsToReturn,
|
expectAllPostCallsToReturn,
|
||||||
callGet,
|
callGet,
|
||||||
partial,
|
partial,
|
||||||
} = require("../../helpers/call-helpers")
|
} = require("../../../helpers/call-helpers")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ describe("/admin/orders", () => {
|
|||||||
let dbConnection
|
let dbConnection
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd })
|
||||||
medusaProcess = await setupServer({ cwd })
|
medusaProcess = await setupServer({ cwd })
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const { PriceList, CustomerGroup } = require("@medusajs/medusa")
|
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../helpers/setup-server")
|
||||||
|
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { useDb, initDb } = require("../../../helpers/use-db")
|
const { useDb, initDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
@@ -9,14 +9,17 @@ const {
|
|||||||
simpleProductFactory,
|
simpleProductFactory,
|
||||||
simplePriceListFactory,
|
simplePriceListFactory,
|
||||||
} = require("../../factories")
|
} = require("../../factories")
|
||||||
const {
|
|
||||||
simpleCustomerGroupFactory,
|
|
||||||
} = require("../../factories/simple-customer-group-factory")
|
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
const customerSeeder = require("../../helpers/customer-seeder")
|
const customerSeeder = require("../../helpers/customer-seeder")
|
||||||
const priceListSeeder = require("../../helpers/price-list-seeder")
|
const priceListSeeder = require("../../helpers/price-list-seeder")
|
||||||
const productSeeder = require("../../helpers/product-seeder")
|
const productSeeder = require("../../helpers/product-seeder")
|
||||||
|
|
||||||
|
const adminReqConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
describe("/admin/price-lists", () => {
|
describe("/admin/price-lists", () => {
|
||||||
@@ -1141,54 +1144,52 @@ describe("/admin/price-lists", () => {
|
|||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.count).toEqual(2)
|
expect(response.data.count).toEqual(2)
|
||||||
expect(response.data.products).toHaveLength(2)
|
expect(response.data.products).toHaveLength(2)
|
||||||
expect(response.data.products).toEqual(
|
expect(response.data.products).toEqual([
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-prod-1",
|
id: "test-prod-1",
|
||||||
variants: [
|
variants: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-variant-1",
|
id: "test-variant-1",
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
price_list_id: "test-list",
|
price_list_id: "test-list",
|
||||||
}),
|
}),
|
||||||
],
|
],)
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-variant-2",
|
id: "test-variant-2",
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-prod-2",
|
id: "test-prod-2",
|
||||||
variants: [
|
variants: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-variant-3",
|
id: "test-variant-3",
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-variant-4",
|
id: "test-variant-4",
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
expect.objectContaining({ currency_code: "usd", amount: 100 }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
price_list_id: "test-list",
|
price_list_id: "test-list",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("lists only product 2", async () => {
|
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 { Region } = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../helpers/setup-server")
|
||||||
|
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
|
const { simpleRegionFactory } = require("../../factories");
|
||||||
|
|
||||||
|
const adminReqConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
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 path = require("path")
|
||||||
const {
|
const {
|
||||||
Region,
|
|
||||||
ShippingProfile,
|
ShippingProfile,
|
||||||
ShippingOption,
|
|
||||||
ShippingOptionRequirement,
|
|
||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../helpers/setup-server")
|
||||||
|
const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
const shippingOptionSeeder = require("../../helpers/shipping-option-seeder")
|
const shippingOptionSeeder = require("../../helpers/shipping-option-seeder")
|
||||||
|
const { simpleShippingOptionFactory, simpleRegionFactory } = require("../../factories")
|
||||||
|
|
||||||
|
const adminReqConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
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",
|
code: "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
feature_flags: [
|
feature_flags: expect.any(Array),
|
||||||
{
|
|
||||||
key: "sales_channels",
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default_currency_code: "usd",
|
default_currency_code: "usd",
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
@@ -133,7 +128,8 @@ describe("/admin/store", () => {
|
|||||||
it("successfully updates default currency code", async () => {
|
it("successfully updates default currency code", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const response = await api.post(
|
const response = await api
|
||||||
|
.post(
|
||||||
"/admin/store",
|
"/admin/store",
|
||||||
{
|
{
|
||||||
default_currency_code: "dkk",
|
default_currency_code: "dkk",
|
||||||
@@ -142,6 +138,7 @@ describe("/admin/store", () => {
|
|||||||
headers: { Authorization: "Bearer test_token " },
|
headers: { Authorization: "Bearer test_token " },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.store).toMatchSnapshot({
|
expect(response.data.store).toMatchSnapshot({
|
||||||
|
|||||||
@@ -250,12 +250,10 @@ describe("/admin/swaps", () => {
|
|||||||
data: {},
|
data: {},
|
||||||
})
|
})
|
||||||
await api.post("/store/carts/cart-test/payment-sessions")
|
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",
|
provider_id: "test-pay",
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("Testing, ", TEST.data.cart.items[0])
|
|
||||||
|
|
||||||
// ********* COMPLETE CART *********
|
// ********* COMPLETE CART *********
|
||||||
const completedOrder = await api.post("/store/carts/cart-test/complete")
|
const completedOrder = await api.post("/store/carts/cart-test/complete")
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ describe("Line Item Adjustments", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Tests database constraints", () => {
|
describe("Tests database constraints", () => {
|
||||||
let cart,
|
let cart
|
||||||
discount,
|
let discount
|
||||||
lineItemId = "line-test"
|
const lineItemId = "line-test"
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await cartSeeder(dbConnection)
|
await cartSeeder(dbConnection)
|
||||||
discount = await simpleDiscountFactory(dbConnection, {
|
discount = await simpleDiscountFactory(dbConnection, {
|
||||||
@@ -113,7 +113,7 @@ describe("Line Item Adjustments", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(createLineItemWithAdjustment()).resolves.toEqual(
|
await expect(createLineItemWithAdjustment()).resolves.toEqual(
|
||||||
expect.anything()
|
expect.anything()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -131,7 +131,7 @@ describe("Line Item Adjustments", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(createAdjustmentNullDiscount()).resolves.toEqual(
|
await expect(createAdjustmentNullDiscount()).resolves.toEqual(
|
||||||
expect.anything()
|
expect.anything()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -155,7 +155,8 @@ describe("Line Item Adjustments", () => {
|
|||||||
discount_id: null,
|
discount_id: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
expect(createAdjustmentsNullDiscount()).resolves.toEqual(
|
|
||||||
|
await expect(createAdjustmentsNullDiscount()).resolves.toEqual(
|
||||||
expect.anything()
|
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,
|
discount_id: discount.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(createDuplicateAdjustment()).rejects.toEqual(
|
await expect(createDuplicateAdjustment()).rejects.toEqual(
|
||||||
expect.objectContaining({ code: "23505" })
|
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",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
File renamed without changes.
+12
-12
@@ -9,29 +9,29 @@ const {
|
|||||||
MoneyAmount,
|
MoneyAmount,
|
||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../../helpers/setup-server")
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
|
||||||
const cartSeeder = require("../../helpers/cart-seeder")
|
const cartSeeder = require("../../../helpers/cart-seeder")
|
||||||
const productSeeder = require("../../helpers/product-seeder")
|
const productSeeder = require("../../../helpers/product-seeder")
|
||||||
const swapSeeder = require("../../helpers/swap-seeder")
|
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||||
const {
|
const {
|
||||||
simpleCartFactory,
|
simpleCartFactory,
|
||||||
simpleRegionFactory,
|
simpleRegionFactory,
|
||||||
simpleProductFactory,
|
simpleProductFactory,
|
||||||
simpleShippingOptionFactory,
|
simpleShippingOptionFactory,
|
||||||
simpleLineItemFactory,
|
simpleLineItemFactory,
|
||||||
} = require("../../factories")
|
} = require("../../../factories")
|
||||||
const {
|
const {
|
||||||
simpleDiscountFactory,
|
simpleDiscountFactory,
|
||||||
} = require("../../factories/simple-discount-factory")
|
} = require("../../../factories/simple-discount-factory")
|
||||||
const {
|
const {
|
||||||
simpleCustomerFactory,
|
simpleCustomerFactory,
|
||||||
} = require("../../factories/simple-customer-factory")
|
} = require("../../../factories/simple-customer-factory")
|
||||||
const {
|
const {
|
||||||
simpleCustomerGroupFactory,
|
simpleCustomerGroupFactory,
|
||||||
} = require("../../factories/simple-customer-group-factory")
|
} = require("../../../factories/simple-customer-group-factory")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe("/store/carts", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd })
|
||||||
medusaProcess = await setupServer({ cwd, verbose: false })
|
medusaProcess = await setupServer({ cwd, verbose: false })
|
||||||
})
|
})
|
||||||
@@ -1813,7 +1813,7 @@ describe("/store/carts", () => {
|
|||||||
type: "swap",
|
type: "swap",
|
||||||
})
|
})
|
||||||
|
|
||||||
const cartWithCustomSo = await manager.save(_cart)
|
await manager.save(_cart)
|
||||||
|
|
||||||
await manager.insert(CustomShippingOption, {
|
await manager.insert(CustomShippingOption, {
|
||||||
id: "another-cso-test",
|
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 path = require("path")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../../helpers/setup-server")
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
simpleOrderFactory,
|
simpleOrderFactory,
|
||||||
@@ -10,7 +10,7 @@ const {
|
|||||||
simpleCartFactory,
|
simpleCartFactory,
|
||||||
simpleProductFactory,
|
simpleProductFactory,
|
||||||
simpleProductTaxRateFactory,
|
simpleProductTaxRateFactory,
|
||||||
} = require("../../factories")
|
} = require("../../../factories")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe("Order Taxes", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd })
|
||||||
medusaProcess = await setupServer({ 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-price-list-factory"
|
||||||
export * from "./simple-batch-job-factory"
|
export * from "./simple-batch-job-factory"
|
||||||
export * from "./simple-sales-channel-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 faker from "faker"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
|
|
||||||
export type DiscuntConditionFactoryData = {
|
export type DiscountConditionFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
rule_id: string
|
rule_id: string
|
||||||
type: DiscountConditionType
|
type: DiscountConditionType
|
||||||
@@ -67,7 +67,7 @@ const getJoinTableResourceIdentifiers = (type: string) => {
|
|||||||
|
|
||||||
export const simpleDiscountConditionFactory = async (
|
export const simpleDiscountConditionFactory = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
data: DiscuntConditionFactoryData,
|
data: DiscountConditionFactoryData,
|
||||||
seed?: number
|
seed?: number
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (typeof seed !== "undefined") {
|
if (typeof seed !== "undefined") {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
import {
|
import {
|
||||||
DiscuntConditionFactoryData,
|
DiscountConditionFactoryData,
|
||||||
simpleDiscountConditionFactory,
|
simpleDiscountConditionFactory,
|
||||||
} from "./simple-discount-condition-factory"
|
} from "./simple-discount-condition-factory"
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export type DiscountRuleFactoryData = {
|
|||||||
type?: DiscountRuleType
|
type?: DiscountRuleType
|
||||||
value?: number
|
value?: number
|
||||||
allocation?: AllocationType
|
allocation?: AllocationType
|
||||||
conditions: DiscuntConditionFactoryData[]
|
conditions: DiscountConditionFactoryData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiscountFactoryData = {
|
export type DiscountFactoryData = {
|
||||||
@@ -24,6 +24,8 @@ export type DiscountFactoryData = {
|
|||||||
is_dynamic?: boolean
|
is_dynamic?: boolean
|
||||||
rule?: DiscountRuleFactoryData
|
rule?: DiscountRuleFactoryData
|
||||||
regions?: string[]
|
regions?: string[]
|
||||||
|
starts_at?: Date
|
||||||
|
ends_at?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleDiscountFactory = async (
|
export const simpleDiscountFactory = async (
|
||||||
@@ -37,7 +39,7 @@ export const simpleDiscountFactory = async (
|
|||||||
|
|
||||||
const manager = connection.manager
|
const manager = connection.manager
|
||||||
|
|
||||||
const ruleData = data.rule ?? {}
|
const ruleData = data.rule ?? ({} as DiscountRuleFactoryData)
|
||||||
const ruleToSave = manager.create(DiscountRule, {
|
const ruleToSave = manager.create(DiscountRule, {
|
||||||
type: ruleData.type ?? DiscountRuleType.PERCENTAGE,
|
type: ruleData.type ?? DiscountRuleType.PERCENTAGE,
|
||||||
value: ruleData.value ?? 10,
|
value: ruleData.value ?? 10,
|
||||||
@@ -63,8 +65,9 @@ export const simpleDiscountFactory = async (
|
|||||||
rule_id: dRule.id,
|
rule_id: dRule.id,
|
||||||
code: data.code ?? "TESTCODE",
|
code: data.code ?? "TESTCODE",
|
||||||
regions: data.regions?.map((r) => ({ id: r })) || [],
|
regions: data.regions?.map((r) => ({ id: r })) || [],
|
||||||
|
starts_at: data.starts_at,
|
||||||
|
ends_at: data.ends_at,
|
||||||
})
|
})
|
||||||
|
|
||||||
const discount = await manager.save(toSave)
|
return await manager.save(toSave)
|
||||||
return discount
|
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ export type LineItemFactoryData = {
|
|||||||
returned_quantity?: boolean
|
returned_quantity?: boolean
|
||||||
tax_lines?: TaxLineFactoryData[]
|
tax_lines?: TaxLineFactoryData[]
|
||||||
adjustments: LineItemAdjustmentFactoryData[]
|
adjustments: LineItemAdjustmentFactoryData[]
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleLineItemFactory = async (
|
export const simpleLineItemFactory = async (
|
||||||
@@ -70,6 +71,7 @@ export const simpleLineItemFactory = async (
|
|||||||
shipped_quantity: data.shipped_quantity || null,
|
shipped_quantity: data.shipped_quantity || null,
|
||||||
returned_quantity: data.returned_quantity || null,
|
returned_quantity: data.returned_quantity || null,
|
||||||
adjustments: data.adjustments,
|
adjustments: data.adjustments,
|
||||||
|
includes_tax: data.includes_tax,
|
||||||
})
|
})
|
||||||
|
|
||||||
const line = await manager.save(toSave)
|
const line = await manager.save(toSave)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type PriceListFactoryData = {
|
|||||||
ends_at?: Date
|
ends_at?: Date
|
||||||
customer_groups?: string[]
|
customer_groups?: string[]
|
||||||
prices?: ProductListPrice[]
|
prices?: ProductListPrice[]
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simplePriceListFactory = async (
|
export const simplePriceListFactory = async (
|
||||||
@@ -59,6 +60,7 @@ export const simplePriceListFactory = async (
|
|||||||
starts_at: data.starts_at || null,
|
starts_at: data.starts_at || null,
|
||||||
ends_at: data.ends_at || null,
|
ends_at: data.ends_at || null,
|
||||||
customer_groups: customerGroups,
|
customer_groups: customerGroups,
|
||||||
|
includes_tax: data.includes_tax,
|
||||||
}
|
}
|
||||||
|
|
||||||
const toSave = manager.create(PriceList, toCreate)
|
const toSave = manager.create(PriceList, toCreate)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type RegionFactoryData = {
|
|||||||
countries?: string[]
|
countries?: string[]
|
||||||
automatic_taxes?: boolean
|
automatic_taxes?: boolean
|
||||||
gift_cards_taxable?: boolean
|
gift_cards_taxable?: boolean
|
||||||
|
fulfillment_providers?: { id: string }[]
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleRegionFactory = async (
|
export const simpleRegionFactory = async (
|
||||||
@@ -30,7 +32,9 @@ export const simpleRegionFactory = async (
|
|||||||
currency_code: data.currency_code || "usd",
|
currency_code: data.currency_code || "usd",
|
||||||
tax_rate: data.tax_rate || 0,
|
tax_rate: data.tax_rate || 0,
|
||||||
payment_providers: [{ id: "test-pay" }],
|
payment_providers: [{ id: "test-pay" }],
|
||||||
|
fulfillment_providers: data.fulfillment_providers ?? [{ id: "test-ful" }],
|
||||||
gift_cards_taxable: data.gift_cards_taxable ?? true,
|
gift_cards_taxable: data.gift_cards_taxable ?? true,
|
||||||
|
includes_tax: data.includes_tax,
|
||||||
automatic_taxes:
|
automatic_taxes:
|
||||||
typeof data.automatic_taxes !== "undefined" ? data.automatic_taxes : true,
|
typeof data.automatic_taxes !== "undefined" ? data.automatic_taxes : true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ShippingMethodFactoryData = {
|
|||||||
price?: number
|
price?: number
|
||||||
shipping_option: string | ShippingOptionFactoryData
|
shipping_option: string | ShippingOptionFactoryData
|
||||||
tax_lines?: ShippingMethodTaxLine[]
|
tax_lines?: ShippingMethodTaxLine[]
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleShippingMethodFactory = async (
|
export const simpleShippingMethodFactory = async (
|
||||||
@@ -47,6 +48,7 @@ export const simpleShippingMethodFactory = async (
|
|||||||
shipping_option_id: shippingOptionId,
|
shipping_option_id: shippingOptionId,
|
||||||
data: data.data || {},
|
data: data.data || {},
|
||||||
price: typeof data.price !== "undefined" ? data.price : 500,
|
price: typeof data.price !== "undefined" ? data.price : 500,
|
||||||
|
includes_tax: data.includes_tax,
|
||||||
})
|
})
|
||||||
|
|
||||||
const shippingMethod = await manager.save(toSave)
|
const shippingMethod = await manager.save(toSave)
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import faker from "faker"
|
|||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
|
|
||||||
export type ShippingOptionFactoryData = {
|
export type ShippingOptionFactoryData = {
|
||||||
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
region_id: string
|
region_id: string
|
||||||
is_return?: boolean
|
is_return?: boolean
|
||||||
is_giftcard?: boolean
|
is_giftcard?: boolean
|
||||||
price?: number
|
price?: number
|
||||||
price_type?: ShippingOptionPriceType
|
price_type?: ShippingOptionPriceType
|
||||||
|
includes_tax?: boolean
|
||||||
data?: object
|
data?: object
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +37,8 @@ export const simpleShippingOptionFactory = async (
|
|||||||
type: ShippingProfileType.GIFT_CARD,
|
type: ShippingProfileType.GIFT_CARD,
|
||||||
})
|
})
|
||||||
|
|
||||||
const created = manager.create(ShippingOption, {
|
const shippingOptionData = {
|
||||||
id: `simple-so-${Math.random() * 1000}`,
|
id: data.id ?? `simple-so-${Math.random() * 1000}`,
|
||||||
name: data.name || "Test Method",
|
name: data.name || "Test Method",
|
||||||
is_return: data.is_return ?? false,
|
is_return: data.is_return ?? false,
|
||||||
region_id: data.region_id,
|
region_id: data.region_id,
|
||||||
@@ -45,7 +47,15 @@ export const simpleShippingOptionFactory = async (
|
|||||||
price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
|
price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
|
||||||
data: data.data ?? {},
|
data: data.data ?? {},
|
||||||
amount: typeof data.price !== "undefined" ? data.price : 500,
|
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 middlewares from "../../middlewares"
|
||||||
import appRoutes from "./apps"
|
import appRoutes from "./apps"
|
||||||
import authRoutes from "./auth"
|
import authRoutes from "./auth"
|
||||||
|
import batchRoutes from "./batch"
|
||||||
import collectionRoutes from "./collections"
|
import collectionRoutes from "./collections"
|
||||||
|
import currencyRoutes from "./currencies"
|
||||||
import customerGroupRoutes from "./customer-groups"
|
import customerGroupRoutes from "./customer-groups"
|
||||||
import customerRoutes from "./customers"
|
import customerRoutes from "./customers"
|
||||||
import discountRoutes from "./discounts"
|
import discountRoutes from "./discounts"
|
||||||
@@ -14,7 +16,6 @@ import noteRoutes from "./notes"
|
|||||||
import notificationRoutes from "./notifications"
|
import notificationRoutes from "./notifications"
|
||||||
import orderRoutes from "./orders"
|
import orderRoutes from "./orders"
|
||||||
import priceListRoutes from "./price-lists"
|
import priceListRoutes from "./price-lists"
|
||||||
import batchRoutes from "./batch"
|
|
||||||
import productTagRoutes from "./product-tags"
|
import productTagRoutes from "./product-tags"
|
||||||
import productTypesRoutes from "./product-types"
|
import productTypesRoutes from "./product-types"
|
||||||
import productRoutes from "./products"
|
import productRoutes from "./products"
|
||||||
@@ -70,6 +71,7 @@ export default (app, container, config) => {
|
|||||||
collectionRoutes(route)
|
collectionRoutes(route)
|
||||||
customerGroupRoutes(route)
|
customerGroupRoutes(route)
|
||||||
customerRoutes(route)
|
customerRoutes(route)
|
||||||
|
currencyRoutes(route)
|
||||||
discountRoutes(route)
|
discountRoutes(route)
|
||||||
draftOrderRoutes(route)
|
draftOrderRoutes(route)
|
||||||
giftCardRoutes(route)
|
giftCardRoutes(route)
|
||||||
@@ -77,15 +79,15 @@ export default (app, container, config) => {
|
|||||||
noteRoutes(route)
|
noteRoutes(route)
|
||||||
notificationRoutes(route)
|
notificationRoutes(route)
|
||||||
orderRoutes(route, featureFlagRouter)
|
orderRoutes(route, featureFlagRouter)
|
||||||
priceListRoutes(route)
|
priceListRoutes(route, featureFlagRouter)
|
||||||
productRoutes(route, featureFlagRouter)
|
productRoutes(route, featureFlagRouter)
|
||||||
productTagRoutes(route)
|
productTagRoutes(route)
|
||||||
productTypesRoutes(route)
|
productTypesRoutes(route)
|
||||||
regionRoutes(route)
|
regionRoutes(route, featureFlagRouter)
|
||||||
returnReasonRoutes(route)
|
returnReasonRoutes(route)
|
||||||
returnRoutes(route)
|
returnRoutes(route)
|
||||||
salesChannelRoutes(route)
|
salesChannelRoutes(route)
|
||||||
shippingOptionRoutes(route)
|
shippingOptionRoutes(route, featureFlagRouter)
|
||||||
shippingProfileRoutes(route)
|
shippingProfileRoutes(route)
|
||||||
storeRoutes(route)
|
storeRoutes(route)
|
||||||
swapRoutes(route)
|
swapRoutes(route)
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator"
|
||||||
import {
|
import {
|
||||||
AdminPriceListPricesCreateReq,
|
AdminPriceListPricesCreateReq,
|
||||||
CreatePriceListInput,
|
CreatePriceListInput,
|
||||||
PriceListStatus,
|
PriceListStatus,
|
||||||
PriceListType,
|
PriceListType,
|
||||||
} from "../../../../types/price-list"
|
} 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 { 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
|
* @oas [post] /price-lists
|
||||||
@@ -98,6 +101,9 @@ import { Type } from "class-transformer"
|
|||||||
* id:
|
* id:
|
||||||
* description: The ID of a customer group
|
* description: The ID of a customer group
|
||||||
* type: string
|
* type: string
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of price list"
|
||||||
|
* type: boolean
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
* - lang: JavaScript
|
* - lang: JavaScript
|
||||||
* label: JS Client
|
* label: JS Client
|
||||||
@@ -215,4 +221,10 @@ export class AdminPostPriceListsPriceListReq {
|
|||||||
@Type(() => CustomerGroup)
|
@Type(() => CustomerGroup)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
customer_groups?: CustomerGroup[]
|
customer_groups?: CustomerGroup[]
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,18 @@ import {
|
|||||||
defaultAdminProductRelations,
|
defaultAdminProductRelations,
|
||||||
} from "../products"
|
} from "../products"
|
||||||
import { AdminPostPriceListsPriceListReq } from "./create-price-list"
|
import { AdminPostPriceListsPriceListReq } from "./create-price-list"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/price-lists", route)
|
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("/:id", middlewares.wrap(require("./get-price-list").default))
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import {
|
|
||||||
AdminPriceListPricesUpdateReq,
|
|
||||||
PriceListStatus,
|
|
||||||
PriceListType,
|
|
||||||
} from "../../../../types/price-list"
|
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
|
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 { Type } from "class-transformer"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { EntityManager } from "typeorm"
|
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}
|
* @oas [post] /price-lists/{id}
|
||||||
@@ -99,6 +102,9 @@ import { EntityManager } from "typeorm"
|
|||||||
* id:
|
* id:
|
||||||
* description: The ID of a customer group
|
* description: The ID of a customer group
|
||||||
* type: string
|
* type: string
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of price list"
|
||||||
|
* type: boolean
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
* - lang: JavaScript
|
* - lang: JavaScript
|
||||||
* label: JS Client
|
* label: JS Client
|
||||||
@@ -213,4 +219,10 @@ export class AdminPostPriceListsPriceListPriceListReq {
|
|||||||
@Type(() => CustomerGroup)
|
@Type(() => CustomerGroup)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
customer_groups?: CustomerGroup[]
|
customer_groups?: CustomerGroup[]
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
|
import { defaultAdminRegionFields, defaultAdminRegionRelations } from "."
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { Region } from "../../../.."
|
import { Region } from "../../../.."
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
import RegionService from "../../../../services/region"
|
import RegionService from "../../../../services/region"
|
||||||
import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||||
|
import { validator } from "../../../../utils/validator"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /regions
|
* @oas [post] /regions
|
||||||
@@ -61,6 +63,9 @@ import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* type: string
|
* type: string
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of region"
|
||||||
|
* type: boolean
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
* - lang: JavaScript
|
* - lang: JavaScript
|
||||||
* label: JS Client
|
* label: JS Client
|
||||||
@@ -179,6 +184,12 @@ export class AdminPostRegionsReq {
|
|||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
countries: string[]
|
countries: string[]
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
|
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import { Region } from "../../../.."
|
|||||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||||
import middlewares from "../../../middlewares"
|
import middlewares from "../../../middlewares"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/regions", route)
|
app.use("/regions", route)
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)) {
|
||||||
|
defaultAdminRegionFields.push("includes_tax")
|
||||||
|
}
|
||||||
|
|
||||||
route.get("/", middlewares.wrap(require("./list-regions").default))
|
route.get("/", middlewares.wrap(require("./list-regions").default))
|
||||||
route.get("/:region_id", middlewares.wrap(require("./get-region").default))
|
route.get("/:region_id", middlewares.wrap(require("./get-region").default))
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import {
|
|||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { EntityManager } from "typeorm"
|
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 RegionService from "../../../../services/region"
|
||||||
import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||||
|
import { validator } from "../../../../utils/validator"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /regions/{id}
|
* @oas [post] /regions/{id}
|
||||||
@@ -49,6 +51,9 @@ import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
|||||||
* tax_rate:
|
* tax_rate:
|
||||||
* description: "The tax rate to use on Orders in the Region."
|
* description: "The tax rate to use on Orders in the Region."
|
||||||
* type: number
|
* type: number
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of region"
|
||||||
|
* type: boolean
|
||||||
* payment_providers:
|
* payment_providers:
|
||||||
* description: "A list of Payment Provider IDs that should be enabled for the Region"
|
* description: "A list of Payment Provider IDs that should be enabled for the Region"
|
||||||
* type: array
|
* type: array
|
||||||
@@ -178,6 +183,12 @@ export class AdminPostRegionsRegionReq {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
countries?: string[]
|
countries?: string[]
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
|
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("POST /admin/sales-channels/:id/products/batch", () => {
|
describe("POST /admin/sales-channels/:id/products/batch", () => {
|
||||||
describe("add product to a sales channel", () => {
|
describe("add product to a sales channel", () => {
|
||||||
@@ -21,7 +22,7 @@ describe("POST /admin/sales-channels/:id/products/batch", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
product_ids: [{ id: "sales_channel_1_product_1" }],
|
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 { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("POST /admin/sales-channels", () => {
|
describe("POST /admin/sales-channels", () => {
|
||||||
describe("successfully get a sales channel", () => {
|
describe("successfully get a sales channel", () => {
|
||||||
@@ -18,7 +19,7 @@ describe("POST /admin/sales-channels", () => {
|
|||||||
name: "sales channel 1 name",
|
name: "sales channel 1 name",
|
||||||
description: "sales channel 1 description",
|
description: "sales channel 1 description",
|
||||||
},
|
},
|
||||||
flags: ["sales_channels"],
|
flags: [SalesChannelFeatureFlag],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
||||||
describe("remove product from a sales channel", () => {
|
describe("remove product from a sales channel", () => {
|
||||||
@@ -19,7 +20,7 @@ describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
product_ids: [{ id: IdMap.getId("sales_channel_1_product_1") }]
|
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 { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("DELETE /admin/sales-channels/:id", () => {
|
describe("DELETE /admin/sales-channels/:id", () => {
|
||||||
describe("successfully delete a sales channel", () => {
|
describe("successfully delete a sales channel", () => {
|
||||||
@@ -16,7 +17,7 @@ describe("DELETE /admin/sales-channels/:id", () => {
|
|||||||
userId: IdMap.getId("admin_user"),
|
userId: IdMap.getId("admin_user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: ["sales_channels"],
|
flags: [SalesChannelFeatureFlag],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("GET /admin/sales-channels/:id", () => {
|
describe("GET /admin/sales-channels/:id", () => {
|
||||||
describe("successfully get a sales channel", () => {
|
describe("successfully get a sales channel", () => {
|
||||||
@@ -16,7 +17,7 @@ describe("GET /admin/sales-channels/:id", () => {
|
|||||||
userId: IdMap.getId("admin_user"),
|
userId: IdMap.getId("admin_user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: ["sales_channels"],
|
flags: [SalesChannelFeatureFlag],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("GET /admin/sales-channels/", () => {
|
describe("GET /admin/sales-channels/", () => {
|
||||||
describe("successfully list the sales channel", () => {
|
describe("successfully list the sales channel", () => {
|
||||||
@@ -16,7 +17,7 @@ describe("GET /admin/sales-channels/", () => {
|
|||||||
userId: IdMap.getId("admin_user"),
|
userId: IdMap.getId("admin_user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: ["sales_channels"],
|
flags: [SalesChannelFeatureFlag],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
describe("POST /admin/regions/:region_id/countries", () => {
|
describe("POST /admin/regions/:region_id/countries", () => {
|
||||||
describe("successful creation", () => {
|
describe("successful creation", () => {
|
||||||
@@ -18,7 +19,7 @@ describe("POST /admin/regions/:region_id/countries", () => {
|
|||||||
userId: IdMap.getId("admin_user"),
|
userId: IdMap.getId("admin_user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: ["sales_channels"],
|
flags: [SalesChannelFeatureFlag],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@@ -9,8 +10,10 @@ import {
|
|||||||
import { defaultFields, defaultRelations } from "."
|
import { defaultFields, defaultRelations } from "."
|
||||||
|
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { EntityManager } from "typeorm"
|
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
|
* @oas [post] /shipping-options
|
||||||
@@ -81,6 +84,9 @@ import { EntityManager } from "typeorm"
|
|||||||
* metadata:
|
* metadata:
|
||||||
* description: An optional set of key-value pairs with additional information.
|
* description: An optional set of key-value pairs with additional information.
|
||||||
* type: object
|
* type: object
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of shipping option"
|
||||||
|
* type: boolean
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
* - lang: JavaScript
|
* - lang: JavaScript
|
||||||
* label: JS Client
|
* label: JS Client
|
||||||
@@ -214,4 +220,10 @@ export class AdminPostShippingOptionsReq {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: object
|
metadata?: object
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,18 @@ import { Router } from "express"
|
|||||||
import { ShippingOption } from "../../../.."
|
import { ShippingOption } from "../../../.."
|
||||||
import { PaginatedResponse, DeleteResponse } from "../../../../types/common"
|
import { PaginatedResponse, DeleteResponse } from "../../../../types/common"
|
||||||
import middlewares from "../../../middlewares"
|
import middlewares from "../../../middlewares"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/shipping-options", route)
|
app.use("/shipping-options", route)
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key)) {
|
||||||
|
defaultFields.push("includes_tax")
|
||||||
|
}
|
||||||
|
|
||||||
route.get("/", middlewares.wrap(require("./list-shipping-options").default))
|
route.get("/", middlewares.wrap(require("./list-shipping-options").default))
|
||||||
route.post("/", middlewares.wrap(require("./create-shipping-option").default))
|
route.post("/", middlewares.wrap(require("./create-shipping-option").default))
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
import { defaultFields, defaultRelations } from "."
|
import { defaultFields, defaultRelations } from "."
|
||||||
|
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { EntityManager } from "typeorm"
|
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}
|
* @oas [post] /shipping-options/{id}
|
||||||
@@ -60,6 +62,9 @@ import { EntityManager } from "typeorm"
|
|||||||
* amount:
|
* amount:
|
||||||
* description: The amount to compare with.
|
* description: The amount to compare with.
|
||||||
* type: integer
|
* type: integer
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Tax included in prices of shipping option"
|
||||||
|
* type: boolean
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
* - lang: JavaScript
|
* - lang: JavaScript
|
||||||
* label: JS Client
|
* label: JS Client
|
||||||
@@ -174,4 +179,10 @@ export class AdminPostShippingOptionsOptionReq {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: object
|
metadata?: object
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
|
||||||
|
IsOptional(),
|
||||||
|
IsBoolean(),
|
||||||
|
])
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,10 @@ import supertest from "supertest"
|
|||||||
import querystring from "querystring"
|
import querystring from "querystring"
|
||||||
import apiLoader from "../loaders/api"
|
import apiLoader from "../loaders/api"
|
||||||
import passportLoader from "../loaders/passport"
|
import passportLoader from "../loaders/passport"
|
||||||
import featureFlagLoader from "../loaders/feature-flags"
|
import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
|
||||||
import servicesLoader from "../loaders/services"
|
import servicesLoader from "../loaders/services"
|
||||||
import strategiesLoader from "../loaders/strategies"
|
import strategiesLoader from "../loaders/strategies"
|
||||||
|
import logger from "../loaders/logger";
|
||||||
|
|
||||||
const adminSessionOpts = {
|
const adminSessionOpts = {
|
||||||
cookieName: "session",
|
cookieName: "session",
|
||||||
@@ -36,8 +37,6 @@ const testApp = express()
|
|||||||
|
|
||||||
const container = createContainer()
|
const container = createContainer()
|
||||||
|
|
||||||
const featureFlagRouter = featureFlagLoader(config)
|
|
||||||
|
|
||||||
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
||||||
container.register("configModule", asValue(config))
|
container.register("configModule", asValue(config))
|
||||||
container.register({
|
container.register({
|
||||||
@@ -60,6 +59,7 @@ testApp.use((req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
featureFlagLoader(config)
|
||||||
servicesLoader({ container, configModule: config })
|
servicesLoader({ container, configModule: config })
|
||||||
strategiesLoader({ container, configModule: config })
|
strategiesLoader({ container, configModule: config })
|
||||||
passportLoader({ app: testApp, 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
|
const { payload, query, headers = {}, flags = [] } = opts
|
||||||
|
|
||||||
flags.forEach((flag) => {
|
flags.forEach((flag) => {
|
||||||
featureFlagRouter.setFlag(flag, true)
|
featureFlagRouter.setFlag(flag.key, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryParams = query && querystring.stringify(query)
|
const queryParams = query && querystring.stringify(query)
|
||||||
@@ -148,6 +148,5 @@ export async function request(method, url, opts = {}) {
|
|||||||
// c[clientSessionOpts.cookieName] &&
|
// c[clientSessionOpts.cookieName] &&
|
||||||
// sessions.util.decode(clientSessionOpts, c[clientSessionOpts.cookieName])
|
// sessions.util.decode(clientSessionOpts, c[clientSessionOpts.cookieName])
|
||||||
// .content
|
// .content
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { MoneyAmount } from ".."
|
import { MoneyAmount } from ".."
|
||||||
import { PriceListType } from "../types/price-list"
|
import { PriceListType } from "../types/price-list"
|
||||||
|
import { TaxServiceRate } from "../types/tax-service"
|
||||||
|
|
||||||
export interface IPriceSelectionStrategy {
|
export interface IPriceSelectionStrategy {
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +56,7 @@ export type PriceSelectionContext = {
|
|||||||
region_id?: string
|
region_id?: string
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
include_discount_prices?: boolean
|
include_discount_prices?: boolean
|
||||||
|
tax_rates?: TaxServiceRate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DefaultPriceType {
|
enum DefaultPriceType {
|
||||||
@@ -67,7 +69,9 @@ export const PriceType = { ...DefaultPriceType, ...PriceListType }
|
|||||||
|
|
||||||
export type PriceSelectionResult = {
|
export type PriceSelectionResult = {
|
||||||
originalPrice: number | null
|
originalPrice: number | null
|
||||||
|
originalPriceIncludesTax?: boolean | null
|
||||||
calculatedPrice: number | null
|
calculatedPrice: number | null
|
||||||
|
calculatedPriceIncludesTax?: boolean | null
|
||||||
calculatedPriceType?: PriceType
|
calculatedPriceType?: PriceType
|
||||||
prices: MoneyAmount[] // prices is an array of all possible price for the input customer and region prices
|
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
|
return !!val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const featureFlagRouter = new FlagRouter({})
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
configModule: { featureFlags?: Record<string, string | boolean> } = {},
|
configModule: { featureFlags?: Record<string, string | boolean> } = {},
|
||||||
logger?: Logger,
|
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 { Column, Entity, PrimaryColumn } from "typeorm"
|
||||||
|
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Currency {
|
export class Currency {
|
||||||
@@ -13,6 +15,9 @@ export class Currency {
|
|||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,4 +50,7 @@ export class Currency {
|
|||||||
* description: "The written name of the currency"
|
* description: "The written name of the currency"
|
||||||
* type: string
|
* type: string
|
||||||
* example: US Dollar
|
* 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
|
* type: object
|
||||||
* description: An optional key-value map with additional details
|
* description: An optional key-value map with additional details
|
||||||
* example: {car: "white"}
|
* 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 { ProductVariant } from "./product-variant"
|
||||||
import { Swap } from "./swap"
|
import { Swap } from "./swap"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
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(`"fulfilled_quantity" <= "quantity"`)
|
||||||
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
|
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
|
||||||
@@ -116,6 +118,9 @@ export class LineItem extends BaseEntity {
|
|||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax: boolean
|
||||||
|
|
||||||
refundable?: number | null
|
refundable?: number | null
|
||||||
subtotal?: number | null
|
subtotal?: number | null
|
||||||
tax_total?: number | null
|
tax_total?: number | null
|
||||||
@@ -275,6 +280,9 @@ export class LineItem extends BaseEntity {
|
|||||||
* type: integer
|
* type: integer
|
||||||
* description: The total of the gift card of the line item
|
* description: The total of the gift card of the line item
|
||||||
* example: 0
|
* example: 0
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Indicates if the line item unit_price include tax"
|
||||||
|
* type: boolean
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class MoneyAmount extends SoftDeletableEntity {
|
|||||||
|
|
||||||
@ManyToOne(() => Currency)
|
@ManyToOne(() => Currency)
|
||||||
@JoinColumn({ name: "currency_code", referencedColumnName: "code" })
|
@JoinColumn({ name: "currency_code", referencedColumnName: "code" })
|
||||||
currency: Currency
|
currency?: Currency
|
||||||
|
|
||||||
@Column({ type: "int" })
|
@Column({ type: "int" })
|
||||||
amount: number
|
amount: number
|
||||||
@@ -58,7 +58,7 @@ export class MoneyAmount extends SoftDeletableEntity {
|
|||||||
|
|
||||||
@ManyToOne(() => Region)
|
@ManyToOne(() => Region)
|
||||||
@JoinColumn({ name: "region_id" })
|
@JoinColumn({ name: "region_id" })
|
||||||
region: Region
|
region?: Region
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): undefined | void {
|
private beforeInsert(): undefined | void {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { CustomerGroup } from "./customer-group"
|
|||||||
import { MoneyAmount } from "./money-amount"
|
import { MoneyAmount } from "./money-amount"
|
||||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
|
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class PriceList extends SoftDeletableEntity {
|
export class PriceList extends SoftDeletableEntity {
|
||||||
@@ -58,6 +60,9 @@ export class PriceList extends SoftDeletableEntity {
|
|||||||
})
|
})
|
||||||
prices: MoneyAmount[]
|
prices: MoneyAmount[]
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax: boolean
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): undefined | void {
|
private beforeInsert(): undefined | void {
|
||||||
this.id = generateEntityId(this.id, "pl")
|
this.id = generateEntityId(this.id, "pl")
|
||||||
@@ -118,6 +123,10 @@ export class PriceList extends SoftDeletableEntity {
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/money_amount"
|
* $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:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* 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 { TaxProvider } from "./tax-provider"
|
||||||
import { TaxRate } from "./tax-rate"
|
import { TaxRate } from "./tax-rate"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
|
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Region extends SoftDeletableEntity {
|
export class Region extends SoftDeletableEntity {
|
||||||
@@ -93,6 +95,9 @@ export class Region extends SoftDeletableEntity {
|
|||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax: boolean
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): void {
|
private beforeInsert(): void {
|
||||||
this.id = generateEntityId(this.id, "reg")
|
this.id = generateEntityId(this.id, "reg")
|
||||||
@@ -170,6 +175,9 @@ export class Region extends SoftDeletableEntity {
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/fulfillment_provider"
|
* $ref: "#/components/schemas/fulfillment_provider"
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Does the prices for the region include tax"
|
||||||
|
* type: boolean
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* 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 { ShippingOption } from "./shipping-option"
|
||||||
import { Swap } from "./swap"
|
import { Swap } from "./swap"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
|
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
|
||||||
@Check(
|
@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`
|
`"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" })
|
@DbAwareColumn({ type: "jsonb" })
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax: boolean
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): void {
|
private beforeInsert(): void {
|
||||||
this.id = generateEntityId(this.id, "sm")
|
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."
|
* 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
|
* type: object
|
||||||
* example: {}
|
* 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 { ShippingProfile } from "./shipping-profile"
|
||||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
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 {
|
export enum ShippingOptionPriceType {
|
||||||
FLAT_RATE = "flat_rate",
|
FLAT_RATE = "flat_rate",
|
||||||
@@ -75,6 +77,9 @@ export class ShippingOption extends SoftDeletableEntity {
|
|||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|
||||||
|
@FeatureFlagColumn(TaxInclusivePricingFeatureFlag.key, { default: false })
|
||||||
|
includes_tax: boolean
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): void {
|
private beforeInsert(): void {
|
||||||
this.id = generateEntityId(this.id, "so")
|
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."
|
* description: "The data needed for the Fulfillment Provider to identify the Shipping Option."
|
||||||
* type: object
|
* type: object
|
||||||
* example: {}
|
* example: {}
|
||||||
|
* includes_tax:
|
||||||
|
* description: "[EXPERIMENTAL] Does the shipping option price include tax"
|
||||||
|
* type: boolean
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EntityRepository, Repository } from "typeorm"
|
import { EntityRepository, Repository } from "typeorm"
|
||||||
import { Currency } from "../models/currency"
|
import { Currency } from "../models"
|
||||||
|
|
||||||
@EntityRepository(Currency)
|
@EntityRepository(Currency)
|
||||||
export class CurrencyRepository extends Repository<Currency> { }
|
export class CurrencyRepository extends Repository<Currency> { }
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import partition from "lodash/partition"
|
import partition from "lodash/partition"
|
||||||
import { MedusaError } from "medusa-core-utils"
|
|
||||||
import {
|
import {
|
||||||
Brackets,
|
Brackets,
|
||||||
EntityRepository,
|
EntityRepository,
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
IsNull,
|
IsNull,
|
||||||
Not,
|
Not,
|
||||||
Repository,
|
Repository,
|
||||||
|
WhereExpressionBuilder,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { MoneyAmount } from "../models/money-amount"
|
import { MoneyAmount } from "../models/money-amount"
|
||||||
import {
|
import {
|
||||||
@@ -126,7 +126,7 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
.leftJoinAndSelect("ma.price_list", "price_list")
|
.leftJoinAndSelect("ma.price_list", "price_list")
|
||||||
.where("ma.variant_id = :variant_id", { variant_id })
|
.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", {
|
const andWhere = subQb.where("ma.price_list_id = :price_list_id", {
|
||||||
price_list_id,
|
price_list_id,
|
||||||
})
|
})
|
||||||
@@ -146,7 +146,8 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
region_id?: string,
|
region_id?: string,
|
||||||
currency_code?: string,
|
currency_code?: string,
|
||||||
customer_id?: string,
|
customer_id?: string,
|
||||||
include_discount_prices?: boolean
|
include_discount_prices?: boolean,
|
||||||
|
include_tax_inclusive_pricing = false
|
||||||
): Promise<[MoneyAmount[], number]> {
|
): Promise<[MoneyAmount[], number]> {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
@@ -154,12 +155,9 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
.leftJoinAndSelect("ma.price_list", "price_list")
|
.leftJoinAndSelect("ma.price_list", "price_list")
|
||||||
.where({ variant_id: variant_id })
|
.where({ variant_id: variant_id })
|
||||||
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
||||||
.andWhere(
|
.andWhere("(price_list.ends_at is null OR price_list.ends_at > :date)", {
|
||||||
"(price_list.ends_at is null OR price_list.ends_at > :date)",
|
|
||||||
{
|
|
||||||
date: date.toUTCString(),
|
date: date.toUTCString(),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
.andWhere(
|
.andWhere(
|
||||||
"(price_list.starts_at is null OR price_list.starts_at < :date)",
|
"(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) {
|
if (region_id || currency_code) {
|
||||||
qb.andWhere(
|
qb.andWhere(
|
||||||
new Brackets((qb) =>
|
new Brackets((qb) =>
|
||||||
@@ -181,14 +184,21 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
|
|
||||||
if (customer_id) {
|
if (customer_id) {
|
||||||
qb.leftJoin("price_list.customer_groups", "cgroup")
|
qb.leftJoin("price_list.customer_groups", "cgroup")
|
||||||
.leftJoin("customer_group_customers", "cgc", "cgc.customer_group_id = cgroup.id")
|
.leftJoin(
|
||||||
.andWhere("(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)", {
|
"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,
|
customer_id,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
qb
|
qb.leftJoin("price_list.customer_groups", "cgroup").andWhere(
|
||||||
.leftJoin("price_list.customer_groups", "cgroup")
|
"cgroup.id is null"
|
||||||
.andWhere("cgroup.id is null")
|
)
|
||||||
}
|
}
|
||||||
return await qb.getManyAndCount()
|
return await qb.getManyAndCount()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ export const carts = {
|
|||||||
total: 1000,
|
total: 1000,
|
||||||
region_id: IdMap.getId("testRegion"),
|
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: {
|
testSwapCart: {
|
||||||
id: IdMap.getId("test-swap"),
|
id: IdMap.getId("test-swap"),
|
||||||
items: [],
|
items: [],
|
||||||
@@ -263,6 +279,9 @@ export const CartServiceMock = {
|
|||||||
if (cartId === IdMap.getId("test-cart2")) {
|
if (cartId === IdMap.getId("test-cart2")) {
|
||||||
return Promise.resolve(carts.testCart)
|
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")
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "cart not found")
|
||||||
}),
|
}),
|
||||||
addLineItem: jest.fn().mockImplementation((cartId, lineItem) => {
|
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 { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import DiscountService from "../discount"
|
import DiscountService from "../discount"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router"
|
||||||
|
|
||||||
|
const featureFlagRouter = new FlagRouter({})
|
||||||
|
|
||||||
describe("DiscountService", () => {
|
describe("DiscountService", () => {
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
@@ -23,6 +26,7 @@ describe("DiscountService", () => {
|
|||||||
discountRepository,
|
discountRepository,
|
||||||
discountRuleRepository,
|
discountRuleRepository,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -160,6 +164,7 @@ describe("DiscountService", () => {
|
|||||||
const discountService = new DiscountService({
|
const discountService = new DiscountService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
discountRepository,
|
discountRepository,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -203,6 +208,7 @@ describe("DiscountService", () => {
|
|||||||
const discountService = new DiscountService({
|
const discountService = new DiscountService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
discountRepository,
|
discountRepository,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -248,6 +254,7 @@ describe("DiscountService", () => {
|
|||||||
discountRepository,
|
discountRepository,
|
||||||
discountRuleRepository,
|
discountRuleRepository,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -345,6 +352,7 @@ describe("DiscountService", () => {
|
|||||||
discountRepository,
|
discountRepository,
|
||||||
discountRuleRepository,
|
discountRuleRepository,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -418,6 +426,7 @@ describe("DiscountService", () => {
|
|||||||
discountRepository,
|
discountRepository,
|
||||||
discountRuleRepository,
|
discountRuleRepository,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -466,6 +475,7 @@ describe("DiscountService", () => {
|
|||||||
discountRepository,
|
discountRepository,
|
||||||
discountRuleRepository,
|
discountRuleRepository,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -509,6 +519,7 @@ describe("DiscountService", () => {
|
|||||||
const discountService = new DiscountService({
|
const discountService = new DiscountService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
discountRepository,
|
discountRepository,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -588,6 +599,7 @@ describe("DiscountService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
discountRepository,
|
discountRepository,
|
||||||
totalsService,
|
totalsService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -763,6 +775,7 @@ describe("DiscountService", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
discountService = new DiscountService({
|
discountService = new DiscountService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
const hasReachedLimitMock = jest.fn().mockImplementation(() => false)
|
const hasReachedLimitMock = jest.fn().mockImplementation(() => false)
|
||||||
const isDisabledMock = jest.fn().mockImplementation(() => false)
|
const isDisabledMock = jest.fn().mockImplementation(() => false)
|
||||||
@@ -892,7 +905,9 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("hasReachedLimit", () => {
|
describe("hasReachedLimit", () => {
|
||||||
const discountService = new DiscountService({})
|
const discountService = new DiscountService({
|
||||||
|
featureFlagRouter,
|
||||||
|
})
|
||||||
|
|
||||||
it("returns true if discount limit is reached", () => {
|
it("returns true if discount limit is reached", () => {
|
||||||
const discount = {
|
const discount = {
|
||||||
@@ -936,7 +951,9 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("isDisabled", () => {
|
describe("isDisabled", () => {
|
||||||
const discountService = new DiscountService({})
|
const discountService = new DiscountService({
|
||||||
|
featureFlagRouter,
|
||||||
|
})
|
||||||
|
|
||||||
it("returns false if discount not disabled", async () => {
|
it("returns false if discount not disabled", async () => {
|
||||||
const discount = {
|
const discount = {
|
||||||
@@ -972,7 +989,9 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("hasNotStarted", () => {
|
describe("hasNotStarted", () => {
|
||||||
const discountService = new DiscountService({})
|
const discountService = new DiscountService({
|
||||||
|
featureFlagRouter,
|
||||||
|
})
|
||||||
|
|
||||||
it("returns true if discount has a future starts_at date", async () => {
|
it("returns true if discount has a future starts_at date", async () => {
|
||||||
const discount = {
|
const discount = {
|
||||||
@@ -1008,7 +1027,9 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("hasExpired", () => {
|
describe("hasExpired", () => {
|
||||||
const discountService = new DiscountService({})
|
const discountService = new DiscountService({
|
||||||
|
featureFlagRouter,
|
||||||
|
})
|
||||||
|
|
||||||
it("returns false if discount has a future ends_at date", async () => {
|
it("returns false if discount has a future ends_at date", async () => {
|
||||||
const discount = {
|
const discount = {
|
||||||
@@ -1068,6 +1089,7 @@ describe("DiscountService", () => {
|
|||||||
|
|
||||||
const discountService = new DiscountService({
|
const discountService = new DiscountService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
discountService.retrieve = retrieveMock
|
discountService.retrieve = retrieveMock
|
||||||
|
|
||||||
@@ -1181,6 +1203,7 @@ describe("DiscountService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
discountConditionRepository,
|
discountConditionRepository,
|
||||||
customerService,
|
customerService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns false on undefined customer id", async () => {
|
it("returns false on undefined customer id", async () => {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router"
|
||||||
import LineItemService from "../line-item"
|
import LineItemService from "../line-item"
|
||||||
|
;[true, false].forEach((isTaxInclusiveEnabled) => {
|
||||||
describe("LineItemService", () => {
|
describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => {
|
||||||
|
describe("LineItemService", () => {
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const lineItemRepository = MockRepository({
|
const lineItemRepository = MockRepository({
|
||||||
create: (data) => data,
|
create: (data) => data,
|
||||||
@@ -70,6 +72,10 @@ describe("LineItemService", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const featureFlagRouter = new FlagRouter({
|
||||||
|
tax_inclusive_pricing: isTaxInclusiveEnabled,
|
||||||
|
})
|
||||||
|
|
||||||
const lineItemService = new LineItemService({
|
const lineItemService = new LineItemService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
pricingService,
|
pricingService,
|
||||||
@@ -77,6 +83,7 @@ describe("LineItemService", () => {
|
|||||||
productVariantService,
|
productVariantService,
|
||||||
regionService,
|
regionService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -282,4 +289,233 @@ describe("LineItemService", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 { MedusaError } from "medusa-core-utils"
|
||||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import PriceListService from "../price-list"
|
|
||||||
import { MoneyAmountRepository } from "../../repositories/money-amount"
|
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({
|
const priceListRepository = MockRepository({
|
||||||
findOne: (q) => {
|
findOne: (q) => {
|
||||||
@@ -42,6 +43,7 @@ describe("PriceListService", () => {
|
|||||||
customerGroupService,
|
customerGroupService,
|
||||||
priceListRepository,
|
priceListRepository,
|
||||||
moneyAmountRepository,
|
moneyAmountRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -121,16 +123,21 @@ describe("PriceListService", () => {
|
|||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const updateRelatedMoneyAmountRepository = MockRepository()
|
const updateRelatedMoneyAmountRepository = MockRepository()
|
||||||
updateRelatedMoneyAmountRepository.create = jest.fn().mockImplementation((rawEntity) => Promise.resolve(rawEntity))
|
updateRelatedMoneyAmountRepository.create = jest
|
||||||
updateRelatedMoneyAmountRepository.save = jest.fn().mockImplementation(() => Promise.resolve())
|
.fn()
|
||||||
updateRelatedMoneyAmountRepository.updatePriceListPrices = (new MoneyAmountRepository()).updatePriceListPrices
|
.mockImplementation((rawEntity) => Promise.resolve(rawEntity))
|
||||||
|
updateRelatedMoneyAmountRepository.save = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
updateRelatedMoneyAmountRepository.updatePriceListPrices = new MoneyAmountRepository().updatePriceListPrices
|
||||||
|
|
||||||
const updateRelatedPriceListService = new PriceListService({
|
const updateRelatedPriceListService = new PriceListService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
customerGroupService,
|
customerGroupService,
|
||||||
priceListRepository,
|
priceListRepository,
|
||||||
moneyAmountRepository: updateRelatedMoneyAmountRepository,
|
moneyAmountRepository: updateRelatedMoneyAmountRepository,
|
||||||
regionService: RegionServiceMock
|
featureFlagRouter: new FlagRouter({}),
|
||||||
|
regionService: RegionServiceMock,
|
||||||
})
|
})
|
||||||
|
|
||||||
it("update only existing price lists and related money amount", async () => {
|
it("update only existing price lists and related money amount", async () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import RegionService from "../region"
|
import { CreateRegionInput } from "../../types/region"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router"
|
||||||
import {
|
import {
|
||||||
EventBusService,
|
EventBusService,
|
||||||
FulfillmentProviderService,
|
FulfillmentProviderService,
|
||||||
PaymentProviderService,
|
PaymentProviderService,
|
||||||
StoreService,
|
StoreService,
|
||||||
} from "../index"
|
} from "../index"
|
||||||
import { CreateRegionInput } from "../../types/region"
|
import RegionService from "../region"
|
||||||
|
|
||||||
const eventBusService = {
|
const eventBusService = {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
@@ -87,6 +88,7 @@ describe("RegionService", () => {
|
|||||||
regionRepository,
|
regionRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
storeService,
|
storeService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
taxProviderRepository,
|
taxProviderRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
@@ -196,6 +198,7 @@ describe("RegionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionRepository,
|
regionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
taxProviderRepository,
|
taxProviderRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
@@ -250,6 +253,7 @@ describe("RegionService", () => {
|
|||||||
currencyRepository,
|
currencyRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
storeService,
|
storeService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -312,6 +316,7 @@ describe("RegionService", () => {
|
|||||||
currencyRepository,
|
currencyRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
storeService,
|
storeService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -367,6 +372,7 @@ describe("RegionService", () => {
|
|||||||
paymentProviderRepository: ppRepository,
|
paymentProviderRepository: ppRepository,
|
||||||
currencyRepository,
|
currencyRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
storeService,
|
storeService,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -427,6 +433,7 @@ describe("RegionService", () => {
|
|||||||
paymentProviderRepository: ppRepository,
|
paymentProviderRepository: ppRepository,
|
||||||
currencyRepository,
|
currencyRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
storeService,
|
storeService,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -472,6 +479,7 @@ describe("RegionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionRepository,
|
regionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -507,6 +515,7 @@ describe("RegionService", () => {
|
|||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
fulfillmentProviderRepository: fpRepository,
|
fulfillmentProviderRepository: fpRepository,
|
||||||
paymentProviderRepository: ppRepository,
|
paymentProviderRepository: ppRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
currencyRepository,
|
currencyRepository,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
storeService,
|
storeService,
|
||||||
@@ -569,6 +578,7 @@ describe("RegionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionRepository,
|
regionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
taxProviderRepository,
|
taxProviderRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
@@ -626,6 +636,7 @@ describe("RegionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionRepository,
|
regionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -661,6 +672,7 @@ describe("RegionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionRepository,
|
regionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||||
import ShippingOptionService from "../shipping-option"
|
import ShippingOptionService from "../shipping-option"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router";
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing";
|
||||||
|
|
||||||
describe("ShippingOptionService", () => {
|
describe("ShippingOptionService", () => {
|
||||||
describe("retrieve", () => {
|
describe("retrieve", () => {
|
||||||
@@ -13,6 +15,7 @@ describe("ShippingOptionService", () => {
|
|||||||
const optionService = new ShippingOptionService({
|
const optionService = new ShippingOptionService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shippingOptionRepository,
|
shippingOptionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully gets shipping option", async () => {
|
it("successfully gets shipping option", async () => {
|
||||||
@@ -60,6 +63,7 @@ describe("ShippingOptionService", () => {
|
|||||||
shippingOptionRepository,
|
shippingOptionRepository,
|
||||||
shippingOptionRequirementRepository,
|
shippingOptionRequirementRepository,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -214,6 +218,7 @@ describe("ShippingOptionService", () => {
|
|||||||
const optionService = new ShippingOptionService({
|
const optionService = new ShippingOptionService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shippingOptionRepository,
|
shippingOptionRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -262,6 +267,7 @@ describe("ShippingOptionService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shippingOptionRepository,
|
shippingOptionRepository,
|
||||||
shippingOptionRequirementRepository,
|
shippingOptionRequirementRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -311,6 +317,7 @@ describe("ShippingOptionService", () => {
|
|||||||
const optionService = new ShippingOptionService({
|
const optionService = new ShippingOptionService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shippingOptionRequirementRepository,
|
shippingOptionRequirementRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -363,6 +370,7 @@ describe("ShippingOptionService", () => {
|
|||||||
shippingOptionRequirementRepository,
|
shippingOptionRequirementRepository,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
regionService,
|
regionService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -526,6 +534,7 @@ describe("ShippingOptionService", () => {
|
|||||||
shippingOptionRepository,
|
shippingOptionRepository,
|
||||||
totalsService,
|
totalsService,
|
||||||
fulfillmentProviderService: providerService,
|
fulfillmentProviderService: providerService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
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 { IdMap } from "medusa-test-utils"
|
||||||
import TotalsService from "../totals"
|
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 = {
|
const discounts = {
|
||||||
total10Percent: {
|
total10Percent: {
|
||||||
@@ -83,11 +87,21 @@ const applyDiscount = (cart, discount) => {
|
|||||||
const calculateAdjustment = (cart, lineItem, discount) => {
|
const calculateAdjustment = (cart, lineItem, discount) => {
|
||||||
let amount = discount.rule.value * lineItem.quantity
|
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") {
|
if (discount.rule.type === "fixed" && discount.rule.allocation === "total") {
|
||||||
let subtotal = cart.items.reduce(
|
let subtotal = cart.items.reduce(
|
||||||
(total, item) => total + item.unit_price * item.quantity,
|
(total, item) => total + price * item.quantity,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const nominator = Math.min(discount.rule.value, subtotal)
|
const nominator = Math.min(discount.rule.value, subtotal)
|
||||||
@@ -99,13 +113,20 @@ const calculateAdjustment = (cart, lineItem, discount) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("TotalsService", () => {
|
describe("TotalsService", () => {
|
||||||
|
const getTaxLinesMock = jest.fn(() => Promise.resolve([{ id: "line1" }]))
|
||||||
|
const featureFlagRouter = new FlagRouter({
|
||||||
|
[TaxInclusivePricingFeatureFlag.key]: false,
|
||||||
|
})
|
||||||
|
|
||||||
const container = {
|
const container = {
|
||||||
taxProviderService: {
|
taxProviderService: {
|
||||||
withTransaction: function () {
|
withTransaction: function() {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
getTaxLines: getTaxLinesMock,
|
||||||
},
|
},
|
||||||
taxCalculationStrategy: {},
|
taxCalculationStrategy: {},
|
||||||
|
featureFlagRouter,
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getAllocationItemDiscounts", () => {
|
describe("getAllocationItemDiscounts", () => {
|
||||||
@@ -278,7 +299,7 @@ describe("TotalsService", () => {
|
|||||||
it("calculate total percentage discount", async () => {
|
it("calculate total percentage discount", async () => {
|
||||||
discountCart.discounts.push(discounts.total10Percent)
|
discountCart.discounts.push(discounts.total10Percent)
|
||||||
let cart = applyDiscount(discountCart, discounts.total10Percent)
|
let cart = applyDiscount(discountCart, discounts.total10Percent)
|
||||||
res = totalsService.getDiscountTotal(cart)
|
res = await totalsService.getDiscountTotal(cart)
|
||||||
|
|
||||||
expect(res).toEqual(28)
|
expect(res).toEqual(28)
|
||||||
})
|
})
|
||||||
@@ -288,7 +309,7 @@ describe("TotalsService", () => {
|
|||||||
it("calculate item fixed discount", async () => {
|
it("calculate item fixed discount", async () => {
|
||||||
discountCart.discounts.push(discounts.item2Fixed)
|
discountCart.discounts.push(discounts.item2Fixed)
|
||||||
let cart = applyDiscount(discountCart, discounts.item2Fixed)
|
let cart = applyDiscount(discountCart, discounts.item2Fixed)
|
||||||
res = totalsService.getDiscountTotal(cart)
|
res = await totalsService.getDiscountTotal(cart)
|
||||||
|
|
||||||
expect(res).toEqual(40)
|
expect(res).toEqual(40)
|
||||||
})
|
})
|
||||||
@@ -296,7 +317,7 @@ describe("TotalsService", () => {
|
|||||||
it("calculate item percentage discount", async () => {
|
it("calculate item percentage discount", async () => {
|
||||||
discountCart.discounts.push(discounts.item10Percent)
|
discountCart.discounts.push(discounts.item10Percent)
|
||||||
let cart = applyDiscount(discountCart, discounts.item10Percent)
|
let cart = applyDiscount(discountCart, discounts.item10Percent)
|
||||||
res = totalsService.getDiscountTotal(cart)
|
res = await totalsService.getDiscountTotal(cart)
|
||||||
|
|
||||||
expect(res).toEqual(28)
|
expect(res).toEqual(28)
|
||||||
})
|
})
|
||||||
@@ -304,26 +325,26 @@ describe("TotalsService", () => {
|
|||||||
it("calculate total fixed discount", async () => {
|
it("calculate total fixed discount", async () => {
|
||||||
discountCart.discounts.push(discounts.total10Fixed)
|
discountCart.discounts.push(discounts.total10Fixed)
|
||||||
let cart = applyDiscount(discountCart, discounts.total10Fixed)
|
let cart = applyDiscount(discountCart, discounts.total10Fixed)
|
||||||
res = totalsService.getDiscountTotal(cart)
|
res = await totalsService.getDiscountTotal(cart)
|
||||||
|
|
||||||
expect(res).toEqual(10)
|
expect(res).toEqual(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("ignores discount if expired", async () => {
|
it("ignores discount if expired", async () => {
|
||||||
discountCart.discounts.push(discounts.expiredDiscount)
|
discountCart.discounts.push(discounts.expiredDiscount)
|
||||||
res = totalsService.getDiscountTotal(discountCart)
|
res = await totalsService.getDiscountTotal(discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(0)
|
expect(res).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns 0 if no discounts are applied", async () => {
|
it("returns 0 if no discounts are applied", async () => {
|
||||||
res = totalsService.getDiscountTotal(discountCart)
|
res = await totalsService.getDiscountTotal(discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(0)
|
expect(res).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns 0 if no items are in cart", async () => {
|
it("returns 0 if no items are in cart", async () => {
|
||||||
res = totalsService.getDiscountTotal({
|
res = await totalsService.getDiscountTotal({
|
||||||
items: [],
|
items: [],
|
||||||
discounts: [discounts.total10Fixed],
|
discounts: [discounts.total10Fixed],
|
||||||
})
|
})
|
||||||
@@ -385,7 +406,7 @@ describe("TotalsService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("calculates refund", async () => {
|
it("calculates refund", async () => {
|
||||||
res = totalsService.getRefundTotal(orderToRefund, [
|
res = await totalsService.getRefundTotal(orderToRefund, [
|
||||||
{
|
{
|
||||||
id: "line2",
|
id: "line2",
|
||||||
unit_price: 100,
|
unit_price: 100,
|
||||||
@@ -447,7 +468,7 @@ describe("TotalsService", () => {
|
|||||||
it("calculates refund with item fixed discount", async () => {
|
it("calculates refund with item fixed discount", async () => {
|
||||||
orderToRefund.discounts.push(discounts.item2Fixed)
|
orderToRefund.discounts.push(discounts.item2Fixed)
|
||||||
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
|
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
|
||||||
res = totalsService.getRefundTotal(order, [
|
res = await totalsService.getRefundTotal(order, [
|
||||||
{
|
{
|
||||||
id: "line2",
|
id: "line2",
|
||||||
unit_price: 100,
|
unit_price: 100,
|
||||||
@@ -467,7 +488,7 @@ describe("TotalsService", () => {
|
|||||||
it("calculates refund with item percentage discount", async () => {
|
it("calculates refund with item percentage discount", async () => {
|
||||||
orderToRefund.discounts.push(discounts.item10Percent)
|
orderToRefund.discounts.push(discounts.item10Percent)
|
||||||
let order = applyDiscount(orderToRefund, discounts.item10Percent)
|
let order = applyDiscount(orderToRefund, discounts.item10Percent)
|
||||||
res = totalsService.getRefundTotal(order, [
|
res = await totalsService.getRefundTotal(order, [
|
||||||
{
|
{
|
||||||
id: "line2",
|
id: "line2",
|
||||||
unit_price: 100,
|
unit_price: 100,
|
||||||
@@ -485,8 +506,9 @@ describe("TotalsService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("throws if line items to return is not in order", async () => {
|
it("throws if line items to return is not in order", async () => {
|
||||||
const work = () =>
|
let errMsg
|
||||||
totalsService.getRefundTotal(orderToRefund, [
|
await totalsService
|
||||||
|
.getRefundTotal(orderToRefund, [
|
||||||
{
|
{
|
||||||
id: "notInOrder",
|
id: "notInOrder",
|
||||||
unit_price: 123,
|
unit_price: 123,
|
||||||
@@ -498,14 +520,213 @@ describe("TotalsService", () => {
|
|||||||
quantity: 1,
|
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", () => {
|
describe("getShippingTotal", () => {
|
||||||
let res
|
const getTaxLinesMock = jest.fn(() =>
|
||||||
const totalsService = new TotalsService(container)
|
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(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
@@ -515,7 +736,7 @@ describe("TotalsService", () => {
|
|||||||
const order = {
|
const order = {
|
||||||
shipping_methods: [
|
shipping_methods: [
|
||||||
{
|
{
|
||||||
_id: IdMap.getId("expensiveShipping"),
|
id: IdMap.getId("expensiveShipping"),
|
||||||
name: "Expensive Shipping",
|
name: "Expensive Shipping",
|
||||||
price: 100,
|
price: 100,
|
||||||
provider_id: "default_provider",
|
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", () => {
|
describe("getTaxTotal", () => {
|
||||||
let res
|
let res
|
||||||
let totalsService
|
let totalsService
|
||||||
@@ -541,7 +763,7 @@ describe("TotalsService", () => {
|
|||||||
|
|
||||||
const cradle = {
|
const cradle = {
|
||||||
taxProviderService: {
|
taxProviderService: {
|
||||||
withTransaction: function () {
|
withTransaction: function() {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
getTaxLines: getTaxLinesMock,
|
getTaxLines: getTaxLinesMock,
|
||||||
@@ -549,6 +771,7 @@ describe("TotalsService", () => {
|
|||||||
taxCalculationStrategy: {
|
taxCalculationStrategy: {
|
||||||
calculate: calculateMock,
|
calculate: calculateMock,
|
||||||
},
|
},
|
||||||
|
featureFlagRouter,
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -597,13 +820,14 @@ describe("TotalsService", () => {
|
|||||||
|
|
||||||
expect(res).toEqual(20)
|
expect(res).toEqual(20)
|
||||||
|
|
||||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
expect(getAllocationMapMock).toHaveBeenCalledTimes(2)
|
||||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
expect(getAllocationMapMock).toHaveBeenNthCalledWith(1, order, {})
|
||||||
|
|
||||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(0)
|
expect(getTaxLinesMock).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
expect(calculateMock).toHaveBeenCalledTimes(1)
|
expect(calculateMock).toHaveBeenCalledTimes(3)
|
||||||
expect(calculateMock).toHaveBeenCalledWith(
|
expect(calculateMock).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
order.items,
|
order.items,
|
||||||
[{ id: "orderline1" }],
|
[{ id: "orderline1" }],
|
||||||
{
|
{
|
||||||
@@ -652,11 +876,15 @@ describe("TotalsService", () => {
|
|||||||
|
|
||||||
expect(res).toEqual(20)
|
expect(res).toEqual(20)
|
||||||
|
|
||||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
expect(getAllocationMapMock).toHaveBeenCalledTimes(2)
|
||||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
expect(getAllocationMapMock).toHaveBeenNthCalledWith(2, order, {
|
||||||
|
exclude_discounts: undefined,
|
||||||
|
exclude_gift_cards: true,
|
||||||
|
})
|
||||||
|
|
||||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(1)
|
expect(getTaxLinesMock).toHaveBeenCalledTimes(2)
|
||||||
expect(getTaxLinesMock).toHaveBeenCalledWith(
|
expect(getTaxLinesMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
[{ quantity: 2, unit_price: 20 }],
|
[{ quantity: 2, unit_price: 20 }],
|
||||||
{
|
{
|
||||||
shipping_address: order.shipping_address,
|
shipping_address: order.shipping_address,
|
||||||
@@ -726,4 +954,137 @@ describe("TotalsService", () => {
|
|||||||
expect(res).toEqual(175)
|
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
|
break
|
||||||
}
|
}
|
||||||
case "shipping_total": {
|
case "shipping_total": {
|
||||||
totals.shipping_total = this.totalsService_.getShippingTotal(cart)
|
totals.shipping_total = await this.totalsService_.getShippingTotal(
|
||||||
|
cart
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "discount_total":
|
case "discount_total":
|
||||||
totals.discount_total = this.totalsService_.getDiscountTotal(cart)
|
totals.discount_total = await this.totalsService_.getDiscountTotal(
|
||||||
|
cart
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case "tax_total":
|
case "tax_total":
|
||||||
totals.tax_total = await this.totalsService_.getTaxTotal(
|
totals.tax_total = await this.totalsService_.getTaxTotal(
|
||||||
@@ -256,13 +260,15 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
case "gift_card_total": {
|
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_total = giftCardBreakdown.total
|
||||||
totals.gift_card_tax_total = giftCardBreakdown.tax_total
|
totals.gift_card_tax_total = giftCardBreakdown.tax_total
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "subtotal":
|
case "subtotal":
|
||||||
totals.subtotal = this.totalsService_.getSubtotal(cart)
|
totals.subtotal = await this.totalsService_.getSubtotal(cart)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -518,7 +524,7 @@ class CartService extends TransactionBaseService {
|
|||||||
.delete(lineItem.id)
|
.delete(lineItem.id)
|
||||||
|
|
||||||
const result = await this.retrieve(cartId, {
|
const result = await this.retrieve(cartId, {
|
||||||
relations: ["items", "discounts", "discounts.rule"],
|
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.refreshAdjustments_(result)
|
await this.refreshAdjustments_(result)
|
||||||
@@ -686,7 +692,7 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const result = await this.retrieve(cartId, {
|
const result = await this.retrieve(cartId, {
|
||||||
relations: ["items", "discounts", "discounts.rule"],
|
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.refreshAdjustments_(result)
|
await this.refreshAdjustments_(result)
|
||||||
@@ -748,7 +754,7 @@ class CartService extends TransactionBaseService {
|
|||||||
.update(lineItemId, lineItemUpdate)
|
.update(lineItemId, lineItemUpdate)
|
||||||
|
|
||||||
const updatedCart = await this.retrieve(cartId, {
|
const updatedCart = await this.retrieve(cartId, {
|
||||||
relations: ["items", "discounts", "discounts.rule"],
|
relations: ["items", "discounts", "discounts.rule", "region"],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.refreshAdjustments_(updatedCart)
|
await this.refreshAdjustments_(updatedCart)
|
||||||
@@ -919,14 +925,14 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hasFreeShipping = cart.discounts.some(
|
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,
|
// if we previously had a free shipping discount and then removed it,
|
||||||
// we need to update shipping methods to original price
|
// we need to update shipping methods to original price
|
||||||
if (
|
if (
|
||||||
previousDiscounts.some(
|
previousDiscounts.some(
|
||||||
({ rule }) => rule.type === "free_shipping"
|
({ rule }) => rule.type === DiscountRuleType.FREE_SHIPPING
|
||||||
) &&
|
) &&
|
||||||
!hasFreeShipping
|
!hasFreeShipping
|
||||||
) {
|
) {
|
||||||
@@ -1229,7 +1235,7 @@ class CartService extends TransactionBaseService {
|
|||||||
default:
|
default:
|
||||||
if (!sawNotShipping) {
|
if (!sawNotShipping) {
|
||||||
sawNotShipping = true
|
sawNotShipping = true
|
||||||
if (rule?.type !== "free_shipping") {
|
if (rule?.type !== DiscountRuleType.FREE_SHIPPING) {
|
||||||
return discount
|
return discount
|
||||||
}
|
}
|
||||||
return discountToParse
|
return discountToParse
|
||||||
@@ -1245,7 +1251,7 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ignore if free shipping
|
// ignore if free shipping
|
||||||
if (rule?.type !== "free_shipping" && cart?.items) {
|
if (rule?.type !== DiscountRuleType.FREE_SHIPPING && cart?.items) {
|
||||||
await this.refreshAdjustments_(cart)
|
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)
|
await this.adjustFreeShipping_(cart, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1770,7 +1780,7 @@ class CartService extends TransactionBaseService {
|
|||||||
// if cart has freeshipping, adjust price
|
// if cart has freeshipping, adjust price
|
||||||
if (
|
if (
|
||||||
updatedCart.discounts.some(
|
updatedCart.discounts.some(
|
||||||
({ rule }) => rule.type === "free_shipping"
|
({ rule }) => rule.type === DiscountRuleType.FREE_SHIPPING
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await this.adjustFreeShipping_(updatedCart, true)
|
await this.adjustFreeShipping_(updatedCart, true)
|
||||||
@@ -2107,7 +2117,7 @@ class CartService extends TransactionBaseService {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const calculationContext = this.totalsService_
|
const calculationContext = await this.totalsService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.getCalculationContext(cart)
|
.getCalculationContext(cart)
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,9 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
const result: ClaimOrder = await claimRepo.save(created)
|
const result: ClaimOrder = await claimRepo.save(created)
|
||||||
|
|
||||||
if (result.additional_items && result.additional_items.length) {
|
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({
|
const lineItems = await lineItemServiceTx.list({
|
||||||
id: result.additional_items.map((i) => i.id),
|
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 CustomerService from "./customer"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
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.
|
* Provides layer to manipulate discounts.
|
||||||
@@ -58,6 +60,7 @@ class DiscountService extends TransactionBaseService {
|
|||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
protected readonly regionService_: RegionService
|
protected readonly regionService_: RegionService
|
||||||
protected readonly eventBus_: EventBusService
|
protected readonly eventBus_: EventBusService
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
@@ -71,6 +74,7 @@ class DiscountService extends TransactionBaseService {
|
|||||||
regionService,
|
regionService,
|
||||||
customerService,
|
customerService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter,
|
||||||
}) {
|
}) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -86,6 +90,7 @@ class DiscountService extends TransactionBaseService {
|
|||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
this.customerService_ = customerService
|
this.customerService_ = customerService
|
||||||
this.eventBus_ = eventBusService
|
this.eventBus_ = eventBusService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -579,7 +584,23 @@ class DiscountService extends TransactionBaseService {
|
|||||||
|
|
||||||
const { type, value, allocation } = discount.rule
|
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) {
|
if (type === DiscountRuleType.PERCENTAGE) {
|
||||||
adjustment = Math.round((fullItemPrice / 100) * value)
|
adjustment = Math.round((fullItemPrice / 100) * value)
|
||||||
@@ -590,12 +611,11 @@ class DiscountService extends TransactionBaseService {
|
|||||||
// when a fixed discount should be applied to the total,
|
// when a fixed discount should be applied to the total,
|
||||||
// we create line adjustments for each item with an amount
|
// we create line adjustments for each item with an amount
|
||||||
// relative to the subtotal
|
// relative to the subtotal
|
||||||
const subtotal = this.totalsService_.getSubtotal(cart, {
|
const subtotal = await this.totalsService_.getSubtotal(cart, {
|
||||||
excludeNonDiscounts: true,
|
excludeNonDiscounts: true,
|
||||||
})
|
})
|
||||||
const nominator = Math.min(value, subtotal)
|
const nominator = Math.min(value, subtotal)
|
||||||
const itemRelativeToSubtotal = lineItem.unit_price / subtotal
|
const totalItemPercentage = fullItemPrice / subtotal
|
||||||
const totalItemPercentage = itemRelativeToSubtotal * lineItem.quantity
|
|
||||||
adjustment = Math.round(nominator * totalItemPercentage)
|
adjustment = Math.round(nominator * totalItemPercentage)
|
||||||
} else {
|
} else {
|
||||||
adjustment = value * lineItem.quantity
|
adjustment = value * lineItem.quantity
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { default as BatchJobService } from "./batch-job"
|
|||||||
export { default as CartService } from "./cart"
|
export { default as CartService } from "./cart"
|
||||||
export { default as ClaimItemService } from "./claim-item"
|
export { default as ClaimItemService } from "./claim-item"
|
||||||
export { default as ClaimService } from "./claim"
|
export { default as ClaimService } from "./claim"
|
||||||
|
export { default as CurrencyService } from "./currency"
|
||||||
export { default as CustomShippingOptionService } from "./custom-shipping-option"
|
export { default as CustomShippingOptionService } from "./custom-shipping-option"
|
||||||
export { default as CustomerGroupService } from "./customer-group"
|
export { default as CustomerGroupService } from "./customer-group"
|
||||||
export { default as CustomerService } from "./customer"
|
export { default as CustomerService } from "./customer"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { Cart } from "../models/cart"
|
import {
|
||||||
import { LineItem } from "../models/line-item"
|
Cart,
|
||||||
import { LineItemAdjustment } from "../models/line-item-adjustment"
|
DiscountRuleType,
|
||||||
import { ProductVariant } from "../models/product-variant"
|
LineItem,
|
||||||
|
LineItemAdjustment,
|
||||||
|
ProductVariant,
|
||||||
|
} from "../models"
|
||||||
import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment"
|
import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment"
|
||||||
import { FindConfig } from "../types/common"
|
import { FindConfig } from "../types/common"
|
||||||
import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment"
|
import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment"
|
||||||
@@ -70,8 +73,9 @@ class LineItemAdjustmentService extends BaseService {
|
|||||||
id: string,
|
id: string,
|
||||||
config: FindConfig<LineItemAdjustment> = {}
|
config: FindConfig<LineItemAdjustment> = {}
|
||||||
): Promise<LineItemAdjustment> {
|
): Promise<LineItemAdjustment> {
|
||||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = this.manager_.getCustomRepository(
|
||||||
this.manager_.getCustomRepository(this.lineItemAdjustmentRepo_)
|
this.lineItemAdjustmentRepo_
|
||||||
|
)
|
||||||
|
|
||||||
const query = this.buildQuery_({ id }, config)
|
const query = this.buildQuery_({ id }, config)
|
||||||
const lineItemAdjustment = await lineItemAdjustmentRepo.findOne(query)
|
const lineItemAdjustment = await lineItemAdjustmentRepo.findOne(query)
|
||||||
@@ -93,8 +97,9 @@ class LineItemAdjustmentService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
async create(data: Partial<LineItemAdjustment>): Promise<LineItemAdjustment> {
|
async create(data: Partial<LineItemAdjustment>): Promise<LineItemAdjustment> {
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
this.lineItemAdjustmentRepo_
|
||||||
|
)
|
||||||
|
|
||||||
const lineItemAdjustment = lineItemAdjustmentRepo.create(data)
|
const lineItemAdjustment = lineItemAdjustmentRepo.create(data)
|
||||||
|
|
||||||
@@ -113,8 +118,9 @@ class LineItemAdjustmentService extends BaseService {
|
|||||||
data: Partial<LineItemAdjustment>
|
data: Partial<LineItemAdjustment>
|
||||||
): Promise<LineItemAdjustment> {
|
): Promise<LineItemAdjustment> {
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
this.lineItemAdjustmentRepo_
|
||||||
|
)
|
||||||
|
|
||||||
const lineItemAdjustment = await this.retrieve(id)
|
const lineItemAdjustment = await this.retrieve(id)
|
||||||
|
|
||||||
@@ -163,8 +169,9 @@ class LineItemAdjustmentService extends BaseService {
|
|||||||
selectorOrId: string | FilterableLineItemAdjustmentProps
|
selectorOrId: string | FilterableLineItemAdjustmentProps
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.atomicPhase_(async (manager) => {
|
return this.atomicPhase_(async (manager) => {
|
||||||
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
|
const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(
|
||||||
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
|
this.lineItemAdjustmentRepo_
|
||||||
|
)
|
||||||
|
|
||||||
if (typeof selectorOrId === "string") {
|
if (typeof selectorOrId === "string") {
|
||||||
return await this.delete({ id: selectorOrId })
|
return await this.delete({ id: selectorOrId })
|
||||||
@@ -206,7 +213,7 @@ class LineItemAdjustmentService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [discount] = cart.discounts.filter(
|
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
|
// 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 { BaseService } from "medusa-interfaces"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { DeepPartial } from "typeorm/common/DeepPartial"
|
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 { LineItemRepository } from "../repositories/line-item"
|
||||||
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
||||||
|
import { FindConfig } from "../types/common"
|
||||||
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
import {
|
import {
|
||||||
PricingService,
|
PricingService,
|
||||||
ProductService,
|
ProductService,
|
||||||
RegionService,
|
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
|
RegionService,
|
||||||
} from "./index"
|
} from "./index"
|
||||||
import { CartRepository } from "../repositories/cart"
|
|
||||||
import { LineItem } from "../models/line-item"
|
|
||||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
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 = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -28,6 +30,7 @@ type InjectedDependencies = {
|
|||||||
pricingService: PricingService
|
pricingService: PricingService
|
||||||
regionService: RegionService
|
regionService: RegionService
|
||||||
lineItemAdjustmentService: LineItemAdjustmentService
|
lineItemAdjustmentService: LineItemAdjustmentService
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +44,9 @@ class LineItemService extends BaseService {
|
|||||||
protected readonly cartRepository_: typeof CartRepository
|
protected readonly cartRepository_: typeof CartRepository
|
||||||
protected readonly productVariantService_: ProductVariantService
|
protected readonly productVariantService_: ProductVariantService
|
||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
|
protected readonly pricingService_: PricingService
|
||||||
protected readonly regionService_: RegionService
|
protected readonly regionService_: RegionService
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -54,6 +59,7 @@ class LineItemService extends BaseService {
|
|||||||
regionService,
|
regionService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
lineItemAdjustmentService,
|
lineItemAdjustmentService,
|
||||||
|
featureFlagRouter,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -66,6 +72,7 @@ class LineItemService extends BaseService {
|
|||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
this.cartRepository_ = cartRepository
|
this.cartRepository_ = cartRepository
|
||||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
withTransaction(transactionManager: EntityManager): LineItemService {
|
withTransaction(transactionManager: EntityManager): LineItemService {
|
||||||
@@ -83,6 +90,7 @@ class LineItemService extends BaseService {
|
|||||||
regionService: this.regionService_,
|
regionService: this.regionService_,
|
||||||
cartRepository: this.cartRepository_,
|
cartRepository: this.cartRepository_,
|
||||||
lineItemAdjustmentService: this.lineItemAdjustmentService_,
|
lineItemAdjustmentService: this.lineItemAdjustmentService_,
|
||||||
|
featureFlagRouter: this.featureFlagRouter_,
|
||||||
})
|
})
|
||||||
|
|
||||||
cloned.transactionManager_ = transactionManager
|
cloned.transactionManager_ = transactionManager
|
||||||
@@ -197,6 +205,7 @@ class LineItemService extends BaseService {
|
|||||||
quantity: number,
|
quantity: number,
|
||||||
context: {
|
context: {
|
||||||
unit_price?: number
|
unit_price?: number
|
||||||
|
includes_tax?: boolean
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
cart?: Cart
|
cart?: Cart
|
||||||
@@ -216,6 +225,9 @@ class LineItemService extends BaseService {
|
|||||||
])
|
])
|
||||||
|
|
||||||
let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price
|
let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price
|
||||||
|
|
||||||
|
let unitPriceIncludesTax = false
|
||||||
|
|
||||||
let shouldMerge = false
|
let shouldMerge = false
|
||||||
|
|
||||||
if (context.unit_price === undefined || context.unit_price === null) {
|
if (context.unit_price === undefined || context.unit_price === null) {
|
||||||
@@ -228,7 +240,10 @@ class LineItemService extends BaseService {
|
|||||||
customer_id: context?.customer_id,
|
customer_id: context?.customer_id,
|
||||||
include_discount_prices: true,
|
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> = {
|
const rawLineItem: Partial<LineItem> = {
|
||||||
@@ -244,6 +259,14 @@ class LineItemService extends BaseService {
|
|||||||
should_merge: shouldMerge,
|
should_merge: shouldMerge,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
TaxInclusivePricingFeatureFlag.key
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
rawLineItem.includes_tax = unitPriceIncludesTax
|
||||||
|
}
|
||||||
|
|
||||||
const lineItemRepo = transactionManager.getCustomRepository(
|
const lineItemRepo = transactionManager.getCustomRepository(
|
||||||
this.lineItemRepository_
|
this.lineItemRepository_
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -164,8 +164,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||||
const query = buildQuery(selector, config)
|
const query = buildQuery(selector, config)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||||
this.transformQueryForTotals(config)
|
config
|
||||||
|
)
|
||||||
|
|
||||||
if (select && select.length) {
|
if (select && select.length) {
|
||||||
query.select = select
|
query.select = select
|
||||||
@@ -233,8 +234,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||||
this.transformQueryForTotals(config)
|
config
|
||||||
|
)
|
||||||
|
|
||||||
if (select && select.length) {
|
if (select && select.length) {
|
||||||
query.select = select
|
query.select = select
|
||||||
@@ -252,7 +254,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
return [orders, count]
|
return [orders, count]
|
||||||
}
|
}
|
||||||
|
|
||||||
protected transformQueryForTotals(config: FindConfig<Order>): {
|
protected transformQueryForTotals(
|
||||||
|
config: FindConfig<Order>
|
||||||
|
): {
|
||||||
relations: string[] | undefined
|
relations: string[] | undefined
|
||||||
select: FindConfig<Order>["select"]
|
select: FindConfig<Order>["select"]
|
||||||
totalsToSelect: FindConfig<Order>["select"]
|
totalsToSelect: FindConfig<Order>["select"]
|
||||||
@@ -333,8 +337,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
): Promise<Order> {
|
): Promise<Order> {
|
||||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||||
this.transformQueryForTotals(config)
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
where: { id: orderId },
|
where: { id: orderId },
|
||||||
@@ -373,8 +378,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
): Promise<Order> {
|
): Promise<Order> {
|
||||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||||
this.transformQueryForTotals(config)
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
where: { cart_id: cartId },
|
where: { cart_id: cartId },
|
||||||
@@ -412,8 +418,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
): Promise<Order> {
|
): Promise<Order> {
|
||||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
|
||||||
this.transformQueryForTotals(config)
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
where: { external_id: externalId },
|
where: { external_id: externalId },
|
||||||
@@ -851,8 +858,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.createShippingMethod(optionId, data ?? {}, { order, ...config })
|
.createShippingMethod(optionId, data ?? {}, { order, ...config })
|
||||||
|
|
||||||
const shippingOptionServiceTx =
|
const shippingOptionServiceTx = this.shippingOptionService_.withTransaction(
|
||||||
this.shippingOptionService_.withTransaction(manager)
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
const methods = [newMethod]
|
const methods = [newMethod]
|
||||||
if (shipping_methods.length) {
|
if (shipping_methods.length) {
|
||||||
@@ -1023,8 +1031,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity)
|
await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentProviderServiceTx =
|
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
|
||||||
this.paymentProviderService_.withTransaction(manager)
|
manager
|
||||||
|
)
|
||||||
for (const p of order.payments) {
|
for (const p of order.payments) {
|
||||||
await paymentProviderServiceTx.cancelPayment(p)
|
await paymentProviderServiceTx.cancelPayment(p)
|
||||||
}
|
}
|
||||||
@@ -1064,8 +1073,9 @@ class OrderService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentProviderServiceTx =
|
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
|
||||||
this.paymentProviderService_.withTransaction(manager)
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
const payments: Payment[] = []
|
const payments: Payment[] = []
|
||||||
for (const p of order.payments) {
|
for (const p of order.payments) {
|
||||||
@@ -1218,7 +1228,7 @@ class OrderService extends TransactionBaseService {
|
|||||||
const fulfillments = await this.fulfillmentService_
|
const fulfillments = await this.fulfillmentService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.createFulfillment(
|
.createFulfillment(
|
||||||
order as unknown as CreateFulfillmentOrder,
|
(order as unknown) as CreateFulfillmentOrder,
|
||||||
itemsToFulfill,
|
itemsToFulfill,
|
||||||
{
|
{
|
||||||
metadata,
|
metadata,
|
||||||
@@ -1429,17 +1439,23 @@ class OrderService extends TransactionBaseService {
|
|||||||
for (const totalField of totalsFields) {
|
for (const totalField of totalsFields) {
|
||||||
switch (totalField) {
|
switch (totalField) {
|
||||||
case "shipping_total": {
|
case "shipping_total": {
|
||||||
order.shipping_total = this.totalsService_.getShippingTotal(order)
|
order.shipping_total = await this.totalsService_.getShippingTotal(
|
||||||
|
order
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "gift_card_total": {
|
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_total = giftCardBreakdown.total
|
||||||
order.gift_card_tax_total = giftCardBreakdown.tax_total
|
order.gift_card_tax_total = giftCardBreakdown.tax_total
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "discount_total": {
|
case "discount_total": {
|
||||||
order.discount_total = this.totalsService_.getDiscountTotal(order)
|
order.discount_total = await this.totalsService_.getDiscountTotal(
|
||||||
|
order
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "tax_total": {
|
case "tax_total": {
|
||||||
@@ -1447,7 +1463,7 @@ class OrderService extends TransactionBaseService {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "subtotal": {
|
case "subtotal": {
|
||||||
order.subtotal = this.totalsService_.getSubtotal(order)
|
order.subtotal = await this.totalsService_.getSubtotal(order)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "total": {
|
case "total": {
|
||||||
@@ -1471,36 +1487,48 @@ class OrderService extends TransactionBaseService {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "items.refundable": {
|
case "items.refundable": {
|
||||||
order.items = order.items.map((i) => ({
|
const items: LineItem[] = []
|
||||||
...i,
|
for (const item of order.items) {
|
||||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
items.push({
|
||||||
...i,
|
...item,
|
||||||
quantity: i.quantity - (i.returned_quantity || 0),
|
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||||
|
...item,
|
||||||
|
quantity: item.quantity - (item.returned_quantity || 0),
|
||||||
} as LineItem),
|
} as LineItem),
|
||||||
})) as LineItem[]
|
} as LineItem)
|
||||||
|
}
|
||||||
|
order.items = items
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "swaps.additional_items.refundable": {
|
case "swaps.additional_items.refundable": {
|
||||||
for (const s of order.swaps) {
|
for (const s of order.swaps) {
|
||||||
s.additional_items = s.additional_items.map((i) => ({
|
const items: LineItem[] = []
|
||||||
...i,
|
for (const item of s.additional_items) {
|
||||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
items.push({
|
||||||
...i,
|
...item,
|
||||||
quantity: i.quantity - (i.returned_quantity || 0),
|
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||||
|
...item,
|
||||||
|
quantity: item.quantity - (item.returned_quantity || 0),
|
||||||
} as LineItem),
|
} as LineItem),
|
||||||
})) as LineItem[]
|
} as LineItem)
|
||||||
|
}
|
||||||
|
s.additional_items = items
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "claims.additional_items.refundable": {
|
case "claims.additional_items.refundable": {
|
||||||
for (const c of order.claims) {
|
for (const c of order.claims) {
|
||||||
c.additional_items = c.additional_items.map((i) => ({
|
const items: LineItem[] = []
|
||||||
...i,
|
for (const item of c.additional_items) {
|
||||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
items.push({
|
||||||
...i,
|
...item,
|
||||||
quantity: i.quantity - (i.returned_quantity || 0),
|
refundable: await this.totalsService_.getLineItemRefund(order, {
|
||||||
|
...item,
|
||||||
|
quantity: item.quantity - (item.returned_quantity || 0),
|
||||||
} as LineItem),
|
} as LineItem),
|
||||||
})) as LineItem[]
|
} as LineItem)
|
||||||
|
}
|
||||||
|
c.additional_items = items
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager, FindOperator } from "typeorm"
|
import { DeepPartial, EntityManager, FindOperator } from "typeorm"
|
||||||
import { CustomerGroupService } from "."
|
import { CustomerGroupService } from "."
|
||||||
import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
|
import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
|
||||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||||
@@ -24,6 +24,8 @@ import { FilterableProductProps } from "../types/product"
|
|||||||
import ProductVariantService from "./product-variant"
|
import ProductVariantService from "./product-variant"
|
||||||
import { FilterableProductVariantProps } from "../types/product-variant"
|
import { FilterableProductVariantProps } from "../types/product-variant"
|
||||||
import { ProductVariantRepository } from "../repositories/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 = {
|
type PriceListConstructorProps = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -34,6 +36,7 @@ type PriceListConstructorProps = {
|
|||||||
priceListRepository: typeof PriceListRepository
|
priceListRepository: typeof PriceListRepository
|
||||||
moneyAmountRepository: typeof MoneyAmountRepository
|
moneyAmountRepository: typeof MoneyAmountRepository
|
||||||
productVariantRepository: typeof ProductVariantRepository
|
productVariantRepository: typeof ProductVariantRepository
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +54,7 @@ class PriceListService extends TransactionBaseService {
|
|||||||
protected readonly priceListRepo_: typeof PriceListRepository
|
protected readonly priceListRepo_: typeof PriceListRepository
|
||||||
protected readonly moneyAmountRepo_: typeof MoneyAmountRepository
|
protected readonly moneyAmountRepo_: typeof MoneyAmountRepository
|
||||||
protected readonly productVariantRepo_: typeof ProductVariantRepository
|
protected readonly productVariantRepo_: typeof ProductVariantRepository
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
@@ -61,6 +65,7 @@ class PriceListService extends TransactionBaseService {
|
|||||||
priceListRepository,
|
priceListRepository,
|
||||||
moneyAmountRepository,
|
moneyAmountRepository,
|
||||||
productVariantRepository,
|
productVariantRepository,
|
||||||
|
featureFlagRouter,
|
||||||
}: PriceListConstructorProps) {
|
}: PriceListConstructorProps) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -73,6 +78,7 @@ class PriceListService extends TransactionBaseService {
|
|||||||
this.priceListRepo_ = priceListRepository
|
this.priceListRepo_ = priceListRepository
|
||||||
this.moneyAmountRepo_ = moneyAmountRepository
|
this.moneyAmountRepo_ = moneyAmountRepository
|
||||||
this.productVariantRepo_ = productVariantRepository
|
this.productVariantRepo_ = productVariantRepository
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,18 +108,34 @@ class PriceListService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Price List
|
* Creates a Price List
|
||||||
* @param {CreatePriceListInput} priceListObject - the Price List to create
|
* @param priceListObject - the Price List to create
|
||||||
* @return {Promise<PriceList>} created Price List
|
* @return created Price List
|
||||||
*/
|
*/
|
||||||
async create(priceListObject: CreatePriceListInput): Promise<PriceList> {
|
async create(
|
||||||
|
priceListObject: CreatePriceListInput
|
||||||
|
): Promise<PriceList | never> {
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
||||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||||
|
|
||||||
const { prices, customer_groups, ...rest } = priceListObject
|
const { prices, customer_groups, includes_tax, ...rest } = priceListObject
|
||||||
|
|
||||||
try {
|
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)
|
const priceList = await priceListRepo.save(entity)
|
||||||
|
|
||||||
@@ -125,11 +147,9 @@ class PriceListService extends TransactionBaseService {
|
|||||||
await this.upsertCustomerGroups_(priceList.id, customer_groups)
|
await this.upsertCustomerGroups_(priceList.id, customer_groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.retrieve(priceList.id, {
|
return await this.retrieve(priceList.id, {
|
||||||
relations: ["prices", "customer_groups"],
|
relations: ["prices", "customer_groups"],
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw formatException(error)
|
throw formatException(error)
|
||||||
}
|
}
|
||||||
@@ -149,7 +169,17 @@ class PriceListService extends TransactionBaseService {
|
|||||||
|
|
||||||
const priceList = await this.retrieve(id, { select: ["id"] })
|
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) {
|
if (prices) {
|
||||||
const prices_ = await this.addCurrencyFromRegion(prices)
|
const prices_ = await this.addCurrencyFromRegion(prices)
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { EntityManager } from "typeorm"
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
import { ProductVariantService, RegionService, TaxProviderService } from "."
|
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 { TransactionBaseService } from "../interfaces"
|
||||||
import {
|
import {
|
||||||
IPriceSelectionStrategy,
|
IPriceSelectionStrategy,
|
||||||
PriceSelectionContext,
|
PriceSelectionContext,
|
||||||
} from "../interfaces/price-selection-strategy"
|
} 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 = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -23,6 +26,7 @@ type InjectedDependencies = {
|
|||||||
taxProviderService: TaxProviderService
|
taxProviderService: TaxProviderService
|
||||||
regionService: RegionService
|
regionService: RegionService
|
||||||
priceSelectionStrategy: IPriceSelectionStrategy
|
priceSelectionStrategy: IPriceSelectionStrategy
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +40,7 @@ class PricingService extends TransactionBaseService {
|
|||||||
protected readonly taxProviderService: TaxProviderService
|
protected readonly taxProviderService: TaxProviderService
|
||||||
protected readonly priceSelectionStrategy: IPriceSelectionStrategy
|
protected readonly priceSelectionStrategy: IPriceSelectionStrategy
|
||||||
protected readonly productVariantService: ProductVariantService
|
protected readonly productVariantService: ProductVariantService
|
||||||
|
protected readonly featureFlagRouter: FlagRouter
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
@@ -43,6 +48,7 @@ class PricingService extends TransactionBaseService {
|
|||||||
taxProviderService,
|
taxProviderService,
|
||||||
regionService,
|
regionService,
|
||||||
priceSelectionStrategy,
|
priceSelectionStrategy,
|
||||||
|
featureFlagRouter,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -52,6 +58,7 @@ class PricingService extends TransactionBaseService {
|
|||||||
this.taxProviderService = taxProviderService
|
this.taxProviderService = taxProviderService
|
||||||
this.priceSelectionStrategy = priceSelectionStrategy
|
this.priceSelectionStrategy = priceSelectionStrategy
|
||||||
this.productVariantService = productVariantService
|
this.productVariantService = productVariantService
|
||||||
|
this.featureFlagRouter = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,17 +122,43 @@ class PricingService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variantPricing.calculated_price !== null) {
|
if (variantPricing.calculated_price !== null) {
|
||||||
const taxAmount = Math.round(variantPricing.calculated_price * rate)
|
const includesTax = !!(
|
||||||
taxedPricing.calculated_tax = taxAmount
|
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 =
|
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) {
|
if (variantPricing.original_price !== null) {
|
||||||
const taxAmount = Math.round(variantPricing.original_price * rate)
|
const includesTax = !!(
|
||||||
taxedPricing.original_tax = taxAmount
|
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 =
|
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
|
return taxedPricing
|
||||||
@@ -137,6 +170,9 @@ class PricingService extends TransactionBaseService {
|
|||||||
context: PricingContext
|
context: PricingContext
|
||||||
): Promise<ProductVariantPricing> {
|
): Promise<ProductVariantPricing> {
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
|
context.price_selection.tax_rates = taxRates
|
||||||
|
|
||||||
const pricing = await this.priceSelectionStrategy
|
const pricing = await this.priceSelectionStrategy
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.calculateVariantPrice(variantId, context.price_selection)
|
.calculateVariantPrice(variantId, context.price_selection)
|
||||||
@@ -146,6 +182,8 @@ class PricingService extends TransactionBaseService {
|
|||||||
original_price: pricing.originalPrice,
|
original_price: pricing.originalPrice,
|
||||||
calculated_price: pricing.calculatedPrice,
|
calculated_price: pricing.calculatedPrice,
|
||||||
calculated_price_type: pricing.calculatedPriceType,
|
calculated_price_type: pricing.calculatedPriceType,
|
||||||
|
original_price_includes_tax: pricing.originalPriceIncludesTax,
|
||||||
|
calculated_price_includes_tax: pricing.calculatedPriceIncludesTax,
|
||||||
original_price_incl_tax: null,
|
original_price_incl_tax: null,
|
||||||
calculated_price_incl_tax: null,
|
calculated_price_incl_tax: null,
|
||||||
original_tax: null,
|
original_tax: null,
|
||||||
@@ -418,14 +456,29 @@ class PricingService extends TransactionBaseService {
|
|||||||
},
|
},
|
||||||
0
|
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,
|
...shippingOption,
|
||||||
price_incl_tax: total,
|
price_incl_tax: totalInclTax,
|
||||||
tax_rates: shippingOptionRates,
|
tax_rates: shippingOptionRates,
|
||||||
|
tax_amount: taxAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,22 +2,24 @@ import { DeepPartial, EntityManager } from "typeorm"
|
|||||||
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
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 { 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 { CountryRepository } from "../repositories/country"
|
||||||
import { CurrencyRepository } from "../repositories/currency"
|
import { CurrencyRepository } from "../repositories/currency"
|
||||||
import { PaymentProviderRepository } from "../repositories/payment-provider"
|
|
||||||
import { FulfillmentProviderRepository } from "../repositories/fulfillment-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 { TaxProviderRepository } from "../repositories/tax-provider"
|
||||||
import FulfillmentProviderService from "./fulfillment-provider"
|
|
||||||
import { Country, Currency, Region } from "../models"
|
|
||||||
import { FindConfig, Selector } from "../types/common"
|
import { FindConfig, Selector } from "../types/common"
|
||||||
import { CreateRegionInput, UpdateRegionInput } from "../types/region"
|
import { CreateRegionInput, UpdateRegionInput } from "../types/region"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
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 { PaymentProviderService } from "./index"
|
||||||
|
import StoreService from "./store"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -25,6 +27,7 @@ type InjectedDependencies = {
|
|||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
paymentProviderService: PaymentProviderService
|
paymentProviderService: PaymentProviderService
|
||||||
fulfillmentProviderService: FulfillmentProviderService
|
fulfillmentProviderService: FulfillmentProviderService
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
|
|
||||||
regionRepository: typeof RegionRepository
|
regionRepository: typeof RegionRepository
|
||||||
countryRepository: typeof CountryRepository
|
countryRepository: typeof CountryRepository
|
||||||
@@ -47,6 +50,7 @@ class RegionService extends TransactionBaseService {
|
|||||||
|
|
||||||
protected manager_: EntityManager
|
protected manager_: EntityManager
|
||||||
protected transactionManager_: EntityManager | undefined
|
protected transactionManager_: EntityManager | undefined
|
||||||
|
protected featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
protected readonly eventBus_: EventBusService
|
protected readonly eventBus_: EventBusService
|
||||||
protected readonly storeService_: StoreService
|
protected readonly storeService_: StoreService
|
||||||
@@ -71,6 +75,7 @@ class RegionService extends TransactionBaseService {
|
|||||||
taxProviderRepository,
|
taxProviderRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
|
featureFlagRouter,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super({
|
super({
|
||||||
manager,
|
manager,
|
||||||
@@ -84,6 +89,7 @@ class RegionService extends TransactionBaseService {
|
|||||||
taxProviderRepository,
|
taxProviderRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
@@ -97,6 +103,8 @@ class RegionService extends TransactionBaseService {
|
|||||||
this.paymentProviderService_ = paymentProviderService
|
this.paymentProviderService_ = paymentProviderService
|
||||||
this.taxProviderRepository_ = taxProviderRepository
|
this.taxProviderRepository_ = taxProviderRepository
|
||||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||||
|
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,10 +123,20 @@ class RegionService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const regionObject = { ...data } as DeepPartial<Region>
|
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)
|
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) {
|
if (currency_code) {
|
||||||
// will throw if currency is not added to store currencies
|
// will throw if currency is not added to store currencies
|
||||||
await this.validateCurrency(currency_code)
|
await this.validateCurrency(currency_code)
|
||||||
@@ -179,10 +197,20 @@ class RegionService extends TransactionBaseService {
|
|||||||
|
|
||||||
const region = await this.retrieve(regionId)
|
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)
|
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) {
|
if (currency_code) {
|
||||||
// will throw if currency is not added to store currencies
|
// will throw if currency is not added to store currencies
|
||||||
await this.validateCurrency(currency_code)
|
await this.validateCurrency(currency_code)
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ class ReturnService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Merchant hasn't specified refund amount so we calculate it
|
// 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
|
const method = data.shipping_method
|
||||||
@@ -469,7 +469,7 @@ class ReturnService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const calculationContext =
|
const calculationContext =
|
||||||
this.totalsService_.getCalculationContext(order)
|
await this.totalsService_.getCalculationContext(order)
|
||||||
|
|
||||||
const taxLines = await this.taxProviderService_
|
const taxLines = await this.taxProviderService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ import {
|
|||||||
import { buildQuery, isDefined, setMetadata } from "../utils"
|
import { buildQuery, isDefined, setMetadata } from "../utils"
|
||||||
import FulfillmentProviderService from "./fulfillment-provider"
|
import FulfillmentProviderService from "./fulfillment-provider"
|
||||||
import RegionService from "./region"
|
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.
|
* Provides layer to manipulate profiles.
|
||||||
@@ -32,6 +44,7 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
protected readonly requirementRepository_: typeof ShippingOptionRequirementRepository
|
protected readonly requirementRepository_: typeof ShippingOptionRequirementRepository
|
||||||
protected readonly optionRepository_: typeof ShippingOptionRepository
|
protected readonly optionRepository_: typeof ShippingOptionRepository
|
||||||
protected readonly methodRepository_: typeof ShippingMethodRepository
|
protected readonly methodRepository_: typeof ShippingMethodRepository
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
protected manager_: EntityManager
|
protected manager_: EntityManager
|
||||||
protected transactionManager_: EntityManager | undefined
|
protected transactionManager_: EntityManager | undefined
|
||||||
@@ -43,7 +56,8 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
shippingMethodRepository,
|
shippingMethodRepository,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
regionService,
|
regionService,
|
||||||
}) {
|
featureFlagRouter,
|
||||||
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
|
|
||||||
@@ -53,6 +67,7 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
this.requirementRepository_ = shippingOptionRequirementRepository
|
this.requirementRepository_ = shippingOptionRequirementRepository
|
||||||
this.providerService_ = fulfillmentProviderService
|
this.providerService_ = fulfillmentProviderService
|
||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,6 +300,16 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
price: methodPrice,
|
price: methodPrice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
TaxInclusivePricingFeatureFlag.key
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (typeof option.includes_tax !== "undefined") {
|
||||||
|
toCreate.includes_tax = option.includes_tax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.order) {
|
if (config.order) {
|
||||||
toCreate.order_id = config.order.id
|
toCreate.order_id = config.order.id
|
||||||
}
|
}
|
||||||
@@ -313,10 +338,10 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
|
|
||||||
const created = await methodRepo.save(method)
|
const created = await methodRepo.save(method)
|
||||||
|
|
||||||
return methodRepo.findOne({
|
return (await methodRepo.findOne({
|
||||||
where: { id: created.id },
|
where: { id: created.id },
|
||||||
relations: ["shipping_option"],
|
relations: ["shipping_option"],
|
||||||
}) as unknown as ShippingMethod
|
})) as ShippingMethod
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +430,16 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
option.amount =
|
option.amount =
|
||||||
data.price_type === "calculated" ? null : data.amount ?? null
|
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)
|
const isValid = await this.providerService_.validateOption(option)
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -584,6 +619,16 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
option.admin_only = update.admin_only
|
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_)
|
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
||||||
return await optionRepo.save(option)
|
return await optionRepo.save(option)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { Store } from "../models"
|
import { Currency, Store } from "../models"
|
||||||
import { CurrencyRepository } from "../repositories/currency"
|
import { CurrencyRepository } from "../repositories/currency"
|
||||||
import { StoreRepository } from "../repositories/store"
|
import { StoreRepository } from "../repositories/store"
|
||||||
import { FindConfig } from "../types/common"
|
import { FindConfig } from "../types/common"
|
||||||
import { UpdateStoreInput } from "../types/store"
|
import { UpdateStoreInput } from "../types/store"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import { buildQuery, setMetadata } from "../utils"
|
||||||
import { currencies, Currency } from "../utils/currencies"
|
import { currencies } from "../utils/currencies"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
|
||||||
import {
|
import {
|
||||||
ITaxCalculationStrategy,
|
ITaxCalculationStrategy,
|
||||||
TaxCalculationContext,
|
TaxCalculationContext,
|
||||||
TransactionBaseService,
|
TransactionBaseService,
|
||||||
} from "../interfaces"
|
} from "../interfaces"
|
||||||
import { Cart } from "../models/cart"
|
import {
|
||||||
import { Discount } from "../models/discount"
|
Cart,
|
||||||
import { DiscountRuleType } from "../models/discount-rule"
|
Discount,
|
||||||
import { LineItem } from "../models/line-item"
|
DiscountRuleType,
|
||||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
LineItem,
|
||||||
import { Order } from "../models/order"
|
LineItemTaxLine,
|
||||||
import { ShippingMethod } from "../models/shipping-method"
|
Order,
|
||||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
ShippingMethod,
|
||||||
|
ShippingMethodTaxLine,
|
||||||
|
} from "../models"
|
||||||
import { isCart } from "../types/cart"
|
import { isCart } from "../types/cart"
|
||||||
import { isOrder } from "../types/orders"
|
import { isOrder } from "../types/orders"
|
||||||
import {
|
import {
|
||||||
@@ -23,12 +24,16 @@ import {
|
|||||||
} from "../types/totals"
|
} from "../types/totals"
|
||||||
import TaxProviderService from "./tax-provider"
|
import TaxProviderService from "./tax-provider"
|
||||||
import { EntityManager } from "typeorm"
|
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 = {
|
type ShippingMethodTotals = {
|
||||||
price: number
|
price: number
|
||||||
tax_total: number
|
tax_total: number
|
||||||
total: number
|
total: number
|
||||||
|
subtotal: number
|
||||||
original_total: number
|
original_total: number
|
||||||
original_tax_total: number
|
original_tax_total: number
|
||||||
tax_lines: ShippingMethodTaxLine[]
|
tax_lines: ShippingMethodTaxLine[]
|
||||||
@@ -55,6 +60,7 @@ type LineItemTotals = {
|
|||||||
type LineItemTotalsOptions = {
|
type LineItemTotalsOptions = {
|
||||||
include_tax?: boolean
|
include_tax?: boolean
|
||||||
use_tax_lines?: boolean
|
use_tax_lines?: boolean
|
||||||
|
exclude_gift_cards?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetLineItemTotalOptions = {
|
type GetLineItemTotalOptions = {
|
||||||
@@ -67,6 +73,7 @@ type TotalsServiceProps = {
|
|||||||
taxProviderService: TaxProviderService
|
taxProviderService: TaxProviderService
|
||||||
taxCalculationStrategy: ITaxCalculationStrategy
|
taxCalculationStrategy: ITaxCalculationStrategy
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTotalsOptions = {
|
type GetTotalsOptions = {
|
||||||
@@ -94,24 +101,29 @@ class TotalsService extends TransactionBaseService {
|
|||||||
protected manager_: EntityManager
|
protected manager_: EntityManager
|
||||||
protected transactionManager_: EntityManager
|
protected transactionManager_: EntityManager
|
||||||
|
|
||||||
private taxProviderService_: TaxProviderService
|
protected readonly taxProviderService_: TaxProviderService
|
||||||
private taxCalculationStrategy_: ITaxCalculationStrategy
|
protected readonly taxCalculationStrategy_: ITaxCalculationStrategy
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
taxProviderService,
|
taxProviderService,
|
||||||
taxCalculationStrategy,
|
taxCalculationStrategy,
|
||||||
|
featureFlagRouter,
|
||||||
}: TotalsServiceProps) {
|
}: TotalsServiceProps) {
|
||||||
super({
|
super({
|
||||||
taxProviderService,
|
taxProviderService,
|
||||||
taxCalculationStrategy,
|
taxCalculationStrategy,
|
||||||
manager,
|
manager,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.taxProviderService_ = taxProviderService
|
this.taxProviderService_ = taxProviderService
|
||||||
this.taxCalculationStrategy_ = taxCalculationStrategy
|
this.taxCalculationStrategy_ = taxCalculationStrategy
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +136,14 @@ class TotalsService extends TransactionBaseService {
|
|||||||
cartOrOrder: Cart | Order,
|
cartOrOrder: Cart | Order,
|
||||||
options: GetTotalsOptions = {}
|
options: GetTotalsOptions = {}
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const subtotal = this.getSubtotal(cartOrOrder)
|
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||||
const taxTotal =
|
const taxTotal =
|
||||||
(await this.getTaxTotal(cartOrOrder, options.force_taxes)) || 0
|
(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
|
const giftCardTotal = options.exclude_gift_cards
|
||||||
? { total: 0 }
|
? { total: 0 }
|
||||||
: this.getGiftCardTotal(cartOrOrder)
|
: await this.getGiftCardTotal(cartOrOrder)
|
||||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
const shippingTotal = await this.getShippingTotal(cartOrOrder)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal.total
|
subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal.total
|
||||||
@@ -182,7 +194,7 @@ class TotalsService extends TransactionBaseService {
|
|||||||
cartOrOrder: Cart | Order,
|
cartOrOrder: Cart | Order,
|
||||||
opts: GetShippingMethodTotalsOptions = {}
|
opts: GetShippingMethodTotalsOptions = {}
|
||||||
): Promise<ShippingMethodTotals> {
|
): Promise<ShippingMethodTotals> {
|
||||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
const calculationContext = await this.getCalculationContext(cartOrOrder, {
|
||||||
exclude_shipping: true,
|
exclude_shipping: true,
|
||||||
})
|
})
|
||||||
calculationContext.shipping_methods = [shippingMethod]
|
calculationContext.shipping_methods = [shippingMethod]
|
||||||
@@ -191,64 +203,70 @@ class TotalsService extends TransactionBaseService {
|
|||||||
price: shippingMethod.price,
|
price: shippingMethod.price,
|
||||||
original_total: shippingMethod.price,
|
original_total: shippingMethod.price,
|
||||||
total: shippingMethod.price,
|
total: shippingMethod.price,
|
||||||
|
subtotal: shippingMethod.price,
|
||||||
original_tax_total: 0,
|
original_tax_total: 0,
|
||||||
tax_total: 0,
|
tax_total: 0,
|
||||||
tax_lines: shippingMethod.tax_lines || [],
|
tax_lines: shippingMethod.tax_lines || [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.include_tax) {
|
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 = Math.round(
|
||||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
totals.price * (cartOrOrder.tax_rate / 100)
|
||||||
)
|
)
|
||||||
totals.tax_total = Math.round(
|
totals.tax_total = Math.round(
|
||||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
totals.price * (cartOrOrder.tax_rate / 100)
|
||||||
)
|
)
|
||||||
} else {
|
} else if (totals.tax_lines.length === 0) {
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
taxLines = shippingMethod.tax_lines
|
|
||||||
} else {
|
|
||||||
const orderLines = await this.taxProviderService_
|
const orderLines = await this.taxProviderService_
|
||||||
.withTransaction(this.manager_)
|
.withTransaction(this.manager_)
|
||||||
.getTaxLines(cartOrOrder.items, calculationContext)
|
.getTaxLines(cartOrOrder.items, calculationContext)
|
||||||
|
|
||||||
taxLines = orderLines.filter((ol) => {
|
totals.tax_lines = orderLines.filter((ol) => {
|
||||||
if ("shipping_method_id" in ol) {
|
if ("shipping_method_id" in ol) {
|
||||||
return ol.shipping_method_id === shippingMethod.id
|
return ol.shipping_method_id === shippingMethod.id
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}) as ShippingMethodTaxLine[]
|
}) 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) {
|
if (totals.tax_lines.length > 0) {
|
||||||
totals.original_tax_total =
|
const includesTax =
|
||||||
await this.taxCalculationStrategy_.calculate(
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
TaxInclusivePricingFeatureFlag.key
|
||||||
|
) && shippingMethod.includes_tax
|
||||||
|
|
||||||
|
totals.original_tax_total = await this.taxCalculationStrategy_.calculate(
|
||||||
[],
|
[],
|
||||||
totals.tax_lines,
|
totals.tax_lines,
|
||||||
calculationContext
|
calculationContext
|
||||||
)
|
)
|
||||||
totals.tax_total = totals.original_tax_total
|
totals.tax_total = totals.original_tax_total
|
||||||
|
|
||||||
|
if (includesTax) {
|
||||||
|
totals.subtotal -= totals.tax_total
|
||||||
|
} else {
|
||||||
totals.original_total += totals.original_tax_total
|
totals.original_total += totals.original_tax_total
|
||||||
totals.total += totals.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
|
return totals
|
||||||
@@ -260,21 +278,33 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param opts - options
|
* @param opts - options
|
||||||
* @return the calculated subtotal
|
* @return the calculated subtotal
|
||||||
*/
|
*/
|
||||||
getSubtotal(cartOrOrder: Cart | Order, opts: SubtotalOptions = {}): number {
|
async getSubtotal(
|
||||||
|
cartOrOrder: Cart | Order,
|
||||||
|
opts: SubtotalOptions = {}
|
||||||
|
): Promise<number> {
|
||||||
let subtotal = 0
|
let subtotal = 0
|
||||||
if (!cartOrOrder.items) {
|
if (!cartOrOrder.items) {
|
||||||
return subtotal
|
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 (opts.excludeNonDiscounts) {
|
||||||
if (item.allow_discounts) {
|
if (item.allow_discounts) {
|
||||||
subtotal += item.unit_price * item.quantity
|
subtotal += await getLineItemSubtotal(item)
|
||||||
}
|
}
|
||||||
} else {
|
continue
|
||||||
subtotal += item.unit_price * item.quantity
|
}
|
||||||
|
|
||||||
|
subtotal += await getLineItemSubtotal(item)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return this.rounded(subtotal)
|
return this.rounded(subtotal)
|
||||||
}
|
}
|
||||||
@@ -284,11 +314,23 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param cartOrOrder - cart or order to calculate subtotal for
|
* @param cartOrOrder - cart or order to calculate subtotal for
|
||||||
* @return shipping total
|
* @return shipping total
|
||||||
*/
|
*/
|
||||||
getShippingTotal(cartOrOrder: Cart | Order): number {
|
async getShippingTotal(cartOrOrder: Cart | Order): Promise<number> {
|
||||||
const { shipping_methods } = cartOrOrder
|
const { shipping_methods } = cartOrOrder
|
||||||
return shipping_methods.reduce((acc, next) => {
|
|
||||||
return acc + next.price
|
let total = 0
|
||||||
}, 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculationContext = this.getCalculationContext(cartOrOrder)
|
const calculationContext = await this.getCalculationContext(cartOrOrder)
|
||||||
const giftCardTotal = this.getGiftCardTotal(cartOrOrder)
|
const giftCardTotal = await this.getGiftCardTotal(cartOrOrder)
|
||||||
|
|
||||||
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[]
|
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[]
|
||||||
if (isOrder(cartOrOrder)) {
|
if (isOrder(cartOrOrder)) {
|
||||||
@@ -335,9 +377,9 @@ class TotalsService extends TransactionBaseService {
|
|||||||
|
|
||||||
taxLines = taxLines.concat(shippingTaxLines)
|
taxLines = taxLines.concat(shippingTaxLines)
|
||||||
} else {
|
} else {
|
||||||
const subtotal = this.getSubtotal(cartOrOrder)
|
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
const shippingTotal = await this.getShippingTotal(cartOrOrder)
|
||||||
const discountTotal = this.getDiscountTotal(cartOrOrder)
|
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||||
return this.rounded(
|
return this.rounded(
|
||||||
(subtotal - discountTotal - giftCardTotal.total + shippingTotal) *
|
(subtotal - discountTotal - giftCardTotal.total + shippingTotal) *
|
||||||
(cartOrOrder.tax_rate / 100)
|
(cartOrOrder.tax_rate / 100)
|
||||||
@@ -388,17 +430,17 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param options - controls what should be included in allocation map
|
* @param options - controls what should be included in allocation map
|
||||||
* @return the allocation map for the line items in the cart or order.
|
* @return the allocation map for the line items in the cart or order.
|
||||||
*/
|
*/
|
||||||
getAllocationMap(
|
async getAllocationMap(
|
||||||
orderOrCart: Cart | Order,
|
orderOrCart: Cart | Order,
|
||||||
options: AllocationMapOptions = {}
|
options: AllocationMapOptions = {}
|
||||||
): LineAllocationsMap {
|
): Promise<LineAllocationsMap> {
|
||||||
const allocationMap: LineAllocationsMap = {}
|
const allocationMap: LineAllocationsMap = {}
|
||||||
|
|
||||||
if (!options.exclude_discounts) {
|
if (!options.exclude_discounts) {
|
||||||
let lineDiscounts: LineDiscountAmount[] = []
|
let lineDiscounts: LineDiscountAmount[] = []
|
||||||
|
|
||||||
const discount = orderOrCart.discounts.find(
|
const discount = orderOrCart.discounts?.find(
|
||||||
({ rule }) => rule.type !== "free_shipping"
|
({ rule }) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||||
)
|
)
|
||||||
if (discount) {
|
if (discount) {
|
||||||
lineDiscounts = this.getLineDiscounts(orderOrCart, discount)
|
lineDiscounts = this.getLineDiscounts(orderOrCart, discount)
|
||||||
@@ -424,8 +466,8 @@ class TotalsService extends TransactionBaseService {
|
|||||||
if (!options.exclude_gift_cards) {
|
if (!options.exclude_gift_cards) {
|
||||||
let lineGiftCards: LineDiscountAmount[] = []
|
let lineGiftCards: LineDiscountAmount[] = []
|
||||||
if (orderOrCart.gift_cards && orderOrCart.gift_cards.length) {
|
if (orderOrCart.gift_cards && orderOrCart.gift_cards.length) {
|
||||||
const subtotal = this.getSubtotal(orderOrCart)
|
const subtotal = await this.getSubtotal(orderOrCart)
|
||||||
const giftCardTotal = this.getGiftCardTotal(orderOrCart)
|
const giftCardTotal = await this.getGiftCardTotal(orderOrCart)
|
||||||
|
|
||||||
// If the fixed discount exceeds the subtotal we should
|
// If the fixed discount exceeds the subtotal we should
|
||||||
// calculate a 100% discount
|
// calculate a 100% discount
|
||||||
@@ -480,20 +522,38 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param lineItem - the line item to calculate the refund amount for.
|
* @param lineItem - the line item to calculate the refund amount for.
|
||||||
* @return the line item refund amount.
|
* @return the line item refund amount.
|
||||||
*/
|
*/
|
||||||
getLineItemRefund(order: Order, lineItem: LineItem): number {
|
async getLineItemRefund(order: Order, lineItem: LineItem): Promise<number> {
|
||||||
const allocationMap = this.getAllocationMap(order)
|
const allocationMap = await this.getAllocationMap(order)
|
||||||
|
|
||||||
|
const includesTax =
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
TaxInclusivePricingFeatureFlag.key
|
||||||
|
) && lineItem.includes_tax
|
||||||
|
|
||||||
const discountAmount =
|
const discountAmount =
|
||||||
(allocationMap[lineItem.id]?.discount?.unit_amount || 0) *
|
(allocationMap[lineItem.id]?.discount?.unit_amount || 0) *
|
||||||
lineItem.quantity
|
lineItem.quantity
|
||||||
|
|
||||||
const lineSubtotal =
|
let lineSubtotal = lineItem.unit_price * lineItem.quantity - discountAmount
|
||||||
lineItem.unit_price * lineItem.quantity - discountAmount
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used for backcompat with old tax system
|
* Used for backcompat with old tax system
|
||||||
*/
|
*/
|
||||||
if (order.tax_rate !== null) {
|
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
|
const taxRate = order.tax_rate / 100
|
||||||
return this.rounded(lineSubtotal * (1 + taxRate))
|
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 taxTotal = lineItem.tax_lines.reduce((acc, next) => {
|
||||||
const taxRate = next.rate / 100
|
const taxRate = next.rate / 100
|
||||||
return acc + this.rounded(lineSubtotal * taxRate)
|
return acc + this.rounded(lineSubtotal * taxRate)
|
||||||
@@ -524,7 +601,7 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param lineItems - the line items to calculate refund total for
|
* @param lineItems - the line items to calculate refund total for
|
||||||
* @return the calculated subtotal
|
* @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)
|
let itemIds = order.items.map((i) => i.id)
|
||||||
|
|
||||||
// in case we swap a swap, we need to include swap items
|
// 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) => {
|
const refunds: number[] = []
|
||||||
if (!itemIds.includes(i.id)) {
|
for (const item of lineItems) {
|
||||||
|
if (!itemIds.includes(item.id)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
"Line item does not exist on order"
|
"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))
|
return this.rounded(refunds.reduce((acc, next) => acc + next, 0))
|
||||||
}
|
}
|
||||||
@@ -624,7 +703,7 @@ class TotalsService extends TransactionBaseService {
|
|||||||
lineItem: LineItem,
|
lineItem: LineItem,
|
||||||
discount: Discount
|
discount: Discount
|
||||||
): number {
|
): number {
|
||||||
const matchingDiscount = lineItem.adjustments.find(
|
const matchingDiscount = lineItem.adjustments?.find(
|
||||||
(adjustment) => adjustment.discount_id === discount.id
|
(adjustment) => adjustment.discount_id === discount.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -709,13 +788,24 @@ class TotalsService extends TransactionBaseService {
|
|||||||
cartOrOrder: Cart | Order,
|
cartOrOrder: Cart | Order,
|
||||||
options: LineItemTotalsOptions = {}
|
options: LineItemTotalsOptions = {}
|
||||||
): Promise<LineItemTotals> {
|
): Promise<LineItemTotals> {
|
||||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
const calculationContext = await this.getCalculationContext(cartOrOrder, {
|
||||||
exclude_shipping: true,
|
exclude_shipping: true,
|
||||||
|
exclude_gift_cards: options.exclude_gift_cards,
|
||||||
})
|
})
|
||||||
const lineItemAllocation =
|
const lineItemAllocation =
|
||||||
calculationContext.allocation_map[lineItem.id] || {}
|
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 gift_card_total = lineItemAllocation.gift_card?.amount || 0
|
||||||
const discount_total =
|
const discount_total =
|
||||||
(lineItemAllocation.discount?.unit_amount || 0) * lineItem.quantity
|
(lineItemAllocation.discount?.unit_amount || 0) * lineItem.quantity
|
||||||
@@ -735,14 +825,32 @@ class TotalsService extends TransactionBaseService {
|
|||||||
|
|
||||||
// Tax Information
|
// Tax Information
|
||||||
if (options.include_tax) {
|
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
|
// order from the old tax system. The following is a backward compat
|
||||||
// calculation.
|
// calculation.
|
||||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate !== null) {
|
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate != null) {
|
||||||
lineItemTotals.original_tax_total =
|
const taxRate = cartOrOrder.tax_rate / 100
|
||||||
subtotal * (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 =
|
lineItemTotals.tax_total =
|
||||||
(subtotal - discount_total) * (cartOrOrder.tax_rate / 100)
|
(lineItemTotals.subtotal - discount_total) * taxRate
|
||||||
|
|
||||||
lineItemTotals.total += lineItemTotals.tax_total
|
lineItemTotals.total += lineItemTotals.tax_total
|
||||||
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||||
@@ -795,15 +903,27 @@ class TotalsService extends TransactionBaseService {
|
|||||||
lineItemTotals.tax_lines,
|
lineItemTotals.tax_lines,
|
||||||
calculationContext
|
calculationContext
|
||||||
)
|
)
|
||||||
lineItemTotals.total += lineItemTotals.tax_total
|
|
||||||
|
|
||||||
calculationContext.allocation_map = {} // Don't account for discounts
|
calculationContext.allocation_map = {} // Don't account for discounts
|
||||||
lineItemTotals.original_tax_total =
|
lineItemTotals.original_tax_total = await this.taxCalculationStrategy_.calculate(
|
||||||
await this.taxCalculationStrategy_.calculate(
|
|
||||||
[lineItem],
|
[lineItem],
|
||||||
lineItemTotals.tax_lines,
|
lineItemTotals.tax_lines,
|
||||||
calculationContext
|
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
|
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,7 +971,9 @@ class TotalsService extends TransactionBaseService {
|
|||||||
*/
|
*/
|
||||||
async getGiftCardableAmount(cartOrOrder: Cart | Order): Promise<number> {
|
async getGiftCardableAmount(cartOrOrder: Cart | Order): Promise<number> {
|
||||||
if (cartOrOrder.region?.gift_cards_taxable) {
|
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, {
|
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
|
* @param cartOrOrder - the cart or order to get gift card amount for
|
||||||
* @return the gift card amount applied to the cart or order
|
* @return the gift card amount applied to the cart or order
|
||||||
*/
|
*/
|
||||||
getGiftCardTotal(cartOrOrder: Cart | Order): {
|
async getGiftCardTotal(
|
||||||
|
cartOrOrder: Cart | Order
|
||||||
|
): Promise<{
|
||||||
total: number
|
total: number
|
||||||
tax_total: number
|
tax_total: number
|
||||||
} {
|
}> {
|
||||||
const giftCardable =
|
const subtotal = await this.getSubtotal(cartOrOrder)
|
||||||
this.getSubtotal(cartOrOrder) - this.getDiscountTotal(cartOrOrder)
|
const discountTotal = await this.getDiscountTotal(cartOrOrder)
|
||||||
|
const giftCardable = subtotal - discountTotal
|
||||||
|
|
||||||
if ("gift_card_transactions" in cartOrOrder) {
|
if ("gift_card_transactions" in cartOrOrder) {
|
||||||
// gift_card_transactions only exist on orders so we can
|
// 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
|
* @param cartOrOrder - the cart or order to calculate discounts for
|
||||||
* @return the total discounts amount
|
* @return the total discounts amount
|
||||||
*/
|
*/
|
||||||
getDiscountTotal(cartOrOrder: Cart | Order): number {
|
async getDiscountTotal(cartOrOrder: Cart | Order): Promise<number> {
|
||||||
const subtotal = this.getSubtotal(cartOrOrder, {
|
const subtotal = await this.getSubtotal(cartOrOrder, {
|
||||||
excludeNonDiscounts: true,
|
excludeNonDiscounts: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!cartOrOrder.discounts || !cartOrOrder.discounts.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only support having free shipping and one other discount, so first
|
// we only support having free shipping and one other discount, so first
|
||||||
// find the discount, which is not free shipping.
|
// find the discount, which is not free shipping.
|
||||||
const discount = cartOrOrder.discounts.find(
|
const discount = cartOrOrder.discounts?.find(
|
||||||
({ rule }) => rule.type !== "free_shipping"
|
({ rule }) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!discount) {
|
if (!discount) {
|
||||||
@@ -970,11 +1091,11 @@ class TotalsService extends TransactionBaseService {
|
|||||||
* @param options - options to gather context by
|
* @param options - options to gather context by
|
||||||
* @return the tax calculation context
|
* @return the tax calculation context
|
||||||
*/
|
*/
|
||||||
getCalculationContext(
|
async getCalculationContext(
|
||||||
cartOrOrder: Cart | Order,
|
cartOrOrder: Cart | Order,
|
||||||
options: CalculationContextOptions = {}
|
options: CalculationContextOptions = {}
|
||||||
): TaxCalculationContext {
|
): Promise<TaxCalculationContext> {
|
||||||
const allocationMap = this.getAllocationMap(cartOrOrder, {
|
const allocationMap = await this.getAllocationMap(cartOrOrder, {
|
||||||
exclude_gift_cards: options.exclude_gift_cards,
|
exclude_gift_cards: options.exclude_gift_cards,
|
||||||
exclude_discounts: options.exclude_discounts,
|
exclude_discounts: options.exclude_discounts,
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,11 @@
|
|||||||
import TaxCalculationStrategy from "../tax-calculation"
|
import TaxCalculationStrategy from "../tax-calculation"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router"
|
||||||
|
|
||||||
const toTest = [
|
const toTest = [
|
||||||
|
[
|
||||||
|
"calculates correctly without gift card",
|
||||||
{
|
{
|
||||||
title: "calculates correctly without gift card",
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Subtotal = 2 * 100 = 200
|
* Subtotal = 2 * 100 = 200
|
||||||
* Taxable amount = 200 - 10 = 190
|
* Taxable amount = 200 - 10 = 190
|
||||||
@@ -54,9 +56,10 @@ const toTest = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"calculates correctly with gift card",
|
||||||
{
|
{
|
||||||
title: "calculates correctly with gift card",
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Subtotal = 2 * 100 = 200
|
* Subtotal = 2 * 100 = 200
|
||||||
* Taxable amount = 200 - 10 = 180
|
* Taxable amount = 200 - 10 = 180
|
||||||
@@ -107,15 +110,173 @@ const toTest = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"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("TaxCalculationStrategy", () => {
|
||||||
describe("calculate", () => {
|
describe("calculate", () => {
|
||||||
const calcStrat = new TaxCalculationStrategy()
|
|
||||||
|
|
||||||
test.each(toTest)(
|
test.each(toTest)(
|
||||||
"$title",
|
"%s",
|
||||||
async ({ items, taxLines, context, expected }) => {
|
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)
|
const val = await calcStrat.calculate(items, taxLines, context)
|
||||||
expect(val).toEqual(expected)
|
expect(val).toEqual(expected)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { EntityManager } from "typeorm"
|
||||||
import {
|
import {
|
||||||
AbstractPriceSelectionStrategy,
|
AbstractPriceSelectionStrategy,
|
||||||
IPriceSelectionStrategy,
|
IPriceSelectionStrategy,
|
||||||
@@ -5,18 +6,22 @@ import {
|
|||||||
PriceSelectionResult,
|
PriceSelectionResult,
|
||||||
PriceType,
|
PriceType,
|
||||||
} from "../interfaces/price-selection-strategy"
|
} from "../interfaces/price-selection-strategy"
|
||||||
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||||
import { EntityManager } from "typeorm"
|
import { TaxServiceRate } from "../types/tax-service"
|
||||||
import { isDefined } from "../utils"
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
|
import { isDefined } from "../utils/is-defined"
|
||||||
|
|
||||||
class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||||
private moneyAmountRepository_: typeof MoneyAmountRepository
|
private moneyAmountRepository_: typeof MoneyAmountRepository
|
||||||
|
private featureFlagRouter_: FlagRouter
|
||||||
private manager_: EntityManager
|
private manager_: EntityManager
|
||||||
|
|
||||||
constructor({ manager, moneyAmountRepository }) {
|
constructor({ manager, featureFlagRouter, moneyAmountRepository }) {
|
||||||
super()
|
super()
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.moneyAmountRepository_ = moneyAmountRepository
|
this.moneyAmountRepository_ = moneyAmountRepository
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
withTransaction(manager: EntityManager): IPriceSelectionStrategy {
|
withTransaction(manager: EntityManager): IPriceSelectionStrategy {
|
||||||
@@ -27,12 +32,116 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
|||||||
return new PriceSelectionStrategy({
|
return new PriceSelectionStrategy({
|
||||||
manager: manager,
|
manager: manager,
|
||||||
moneyAmountRepository: this.moneyAmountRepository_,
|
moneyAmountRepository: this.moneyAmountRepository_,
|
||||||
|
featureFlagRouter: this.featureFlagRouter_,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateVariantPrice(
|
async calculateVariantPrice(
|
||||||
variant_id: string,
|
variant_id: string,
|
||||||
context: PriceSelectionContext
|
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> {
|
): Promise<PriceSelectionResult> {
|
||||||
const moneyRepo = this.manager_.getCustomRepository(
|
const moneyRepo = this.manager_.getCustomRepository(
|
||||||
this.moneyAmountRepository_
|
this.moneyAmountRepository_
|
||||||
@@ -65,6 +174,9 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const ma of prices) {
|
for (const ma of prices) {
|
||||||
|
delete ma.currency
|
||||||
|
delete ma.region
|
||||||
|
|
||||||
if (
|
if (
|
||||||
context.region_id &&
|
context.region_id &&
|
||||||
ma.region_id === 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 =>
|
const isValidQuantity = (price, quantity): boolean =>
|
||||||
(isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) ||
|
(isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) ||
|
||||||
(typeof quantity === "undefined" && isValidPriceWithoutQuantity(price))
|
(typeof quantity === "undefined" && isValidPriceWithoutQuantity(price))
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { LineItem } from "../models/line-item"
|
import {
|
||||||
import { ShippingMethod } from "../models/shipping-method"
|
LineItem,
|
||||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
LineItemTaxLine,
|
||||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
ShippingMethod,
|
||||||
import { TaxCalculationContext } from "../interfaces/tax-service"
|
ShippingMethodTaxLine,
|
||||||
import { ITaxCalculationStrategy } from "../interfaces/tax-calculation-strategy"
|
} 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 {
|
class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
|
constructor({ featureFlagRouter }) {
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
|
}
|
||||||
|
|
||||||
async calculate(
|
async calculate(
|
||||||
items: LineItem[],
|
items: LineItem[],
|
||||||
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
|
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
|
||||||
@@ -33,18 +43,47 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
|||||||
context: TaxCalculationContext
|
context: TaxCalculationContext
|
||||||
): number {
|
): number {
|
||||||
let taxTotal = 0
|
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 -=
|
taxableAmount -=
|
||||||
((allocations.discount && allocations.discount.unit_amount) || 0) *
|
((allocations.discount && allocations.discount.unit_amount) || 0) *
|
||||||
i.quantity
|
item.quantity
|
||||||
|
|
||||||
const lineRates = taxLines.filter((tl) => tl.item_id === i.id)
|
for (const filteredTaxLine of filteredTaxLines) {
|
||||||
for (const lineRate of lineRates) {
|
taxTotal += Math.round(
|
||||||
taxTotal += Math.round(taxableAmount * (lineRate.rate / 100))
|
calculatePriceTaxAmount({
|
||||||
|
price: taxableAmount,
|
||||||
|
taxRate: filteredTaxLine.rate / 100,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return taxTotal
|
return taxTotal
|
||||||
@@ -54,12 +93,19 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
|||||||
shipping_methods: ShippingMethod[],
|
shipping_methods: ShippingMethod[],
|
||||||
taxLines: ShippingMethodTaxLine[]
|
taxLines: ShippingMethodTaxLine[]
|
||||||
): number {
|
): number {
|
||||||
|
const taxInclusiveEnabled = this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
TaxInclusivePricingFeatureFlag.key
|
||||||
|
)
|
||||||
|
|
||||||
let taxTotal = 0
|
let taxTotal = 0
|
||||||
for (const sm of shipping_methods) {
|
for (const sm of shipping_methods) {
|
||||||
const amount = sm.price
|
|
||||||
const lineRates = taxLines.filter((tl) => tl.shipping_method_id === sm.id)
|
const lineRates = taxLines.filter((tl) => tl.shipping_method_id === sm.id)
|
||||||
for (const lineRate of lineRates) {
|
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
|
return taxTotal
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type UpdateCurrencyInput = {
|
||||||
|
includes_tax?: boolean
|
||||||
|
}
|
||||||
@@ -126,12 +126,19 @@ export type CreatePriceListInput = {
|
|||||||
customer_groups?: { id: string }[]
|
customer_groups?: { id: string }[]
|
||||||
starts_at?: Date
|
starts_at?: Date
|
||||||
ends_at?: Date
|
ends_at?: Date
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdatePriceListInput = Partial<
|
export type UpdatePriceListInput = Partial<
|
||||||
Pick<
|
Pick<
|
||||||
PriceList,
|
PriceList,
|
||||||
"name" | "description" | "starts_at" | "ends_at" | "status" | "type"
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "starts_at"
|
||||||
|
| "ends_at"
|
||||||
|
| "status"
|
||||||
|
| "type"
|
||||||
|
| "includes_tax"
|
||||||
>
|
>
|
||||||
> & {
|
> & {
|
||||||
prices?: AdminPriceListPricesUpdateReq[]
|
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 { PriceSelectionContext } from "../interfaces/price-selection-strategy"
|
||||||
|
import { MoneyAmount, Product, ProductVariant, ShippingOption } from "../models"
|
||||||
|
import { TaxServiceRate } from "./tax-service"
|
||||||
|
|
||||||
export type ProductVariantPricing = {
|
export type ProductVariantPricing = {
|
||||||
prices: MoneyAmount[]
|
prices: MoneyAmount[]
|
||||||
original_price: number | null
|
original_price: number | null
|
||||||
calculated_price: number | null
|
calculated_price: number | null
|
||||||
|
original_price_includes_tax?: boolean | null
|
||||||
|
calculated_price_includes_tax?: boolean | null
|
||||||
calculated_price_type?: string | null
|
calculated_price_type?: string | null
|
||||||
} & TaxedPricing
|
} & TaxedPricing
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ export type PricingContext = {
|
|||||||
export type ShippingOptionPricing = {
|
export type ShippingOptionPricing = {
|
||||||
price_incl_tax: number | null
|
price_incl_tax: number | null
|
||||||
tax_rates: TaxServiceRate[] | null
|
tax_rates: TaxServiceRate[] | null
|
||||||
|
tax_amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PricedShippingOption = Partial<ShippingOption> &
|
export type PricedShippingOption = Partial<ShippingOption> &
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { FindConfig } from "./common"
|
|
||||||
import { Region } from "../models"
|
|
||||||
|
|
||||||
export type UpdateRegionInput = {
|
export type UpdateRegionInput = {
|
||||||
name?: string
|
name?: string
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
@@ -12,6 +9,7 @@ export type UpdateRegionInput = {
|
|||||||
payment_providers?: string[]
|
payment_providers?: string[]
|
||||||
fulfillment_providers?: string[]
|
fulfillment_providers?: string[]
|
||||||
countries?: string[]
|
countries?: string[]
|
||||||
|
includes_tax?: boolean
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,5 +21,6 @@ export type CreateRegionInput = {
|
|||||||
payment_providers: string[]
|
payment_providers: string[]
|
||||||
fulfillment_providers: string[]
|
fulfillment_providers: string[]
|
||||||
countries: string[]
|
countries: string[]
|
||||||
|
includes_tax?: boolean
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,7 @@ export type CreateShippingOptionInput = {
|
|||||||
profile_id: string
|
profile_id: string
|
||||||
provider_id: string
|
provider_id: string
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
|
includes_tax?: boolean
|
||||||
|
|
||||||
amount?: number
|
amount?: number
|
||||||
is_return?: boolean
|
is_return?: boolean
|
||||||
@@ -71,4 +72,5 @@ export type UpdateShippingOptionInput = {
|
|||||||
provider_id?: string
|
provider_id?: string
|
||||||
profile_id?: string
|
profile_id?: string
|
||||||
data?: string
|
data?: string
|
||||||
|
includes_tax?: boolean
|
||||||
}
|
}
|
||||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user