diff --git a/.changeset/red-islands-bow.md b/.changeset/red-islands-bow.md new file mode 100644 index 0000000000..481c5a09e7 --- /dev/null +++ b/.changeset/red-islands-bow.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Feat(medusa): Optimize the cart creation with time line and therefore the response time diff --git a/integration-tests/api/__tests__/admin/colllections.js b/integration-tests/api/__tests__/admin/colllections.js index d00a5a91fe..149c6a0cd9 100644 --- a/integration-tests/api/__tests__/admin/colllections.js +++ b/integration-tests/api/__tests__/admin/colllections.js @@ -29,7 +29,7 @@ describe("/admin/collections", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: true }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/draft-order/draft-order.js b/integration-tests/api/__tests__/admin/draft-order/draft-order.js index 23bafb0d9e..bba8f59ac8 100644 --- a/integration-tests/api/__tests__/admin/draft-order/draft-order.js +++ b/integration-tests/api/__tests__/admin/draft-order/draft-order.js @@ -26,7 +26,7 @@ describe("/admin/draft-orders", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: true }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/draft-order/ff-tax-inclusive-draft-order.js b/integration-tests/api/__tests__/admin/draft-order/ff-tax-inclusive-draft-order.js index 39662d1592..aeeb01851a 100644 --- a/integration-tests/api/__tests__/admin/draft-order/ff-tax-inclusive-draft-order.js +++ b/integration-tests/api/__tests__/admin/draft-order/ff-tax-inclusive-draft-order.js @@ -29,7 +29,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/draft-orders", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index ba4c71567a..9c8e79e72f 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -40,7 +40,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_ORDER_EDITING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/order/ff-tax-inclusive-pricing.js b/integration-tests/api/__tests__/admin/order/ff-tax-inclusive-pricing.js index 94612d5d2f..7fe74319c7 100644 --- a/integration-tests/api/__tests__/admin/order/ff-tax-inclusive-pricing.js +++ b/integration-tests/api/__tests__/admin/order/ff-tax-inclusive-pricing.js @@ -10,8 +10,8 @@ const adminSeeder = require("../../../helpers/admin-seeder") const { simpleRegionFactory, simpleShippingOptionFactory, - simpleOrderFactory -} = require("../../../factories"); + simpleOrderFactory, +} = require("../../../factories") jest.setTimeout(30000) @@ -24,7 +24,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/orders", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process @@ -50,7 +49,7 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/orders", () => { country_code: "us", } const region = await simpleRegionFactory(dbConnection, { - id: "test-region" + id: "test-region", }) order = await simpleOrderFactory(dbConnection, { id: "test-order", @@ -58,16 +57,19 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/orders", () => { shipping_address: shippingAddress, currency_code: "usd", }) - includesTaxShippingOption = await simpleShippingOptionFactory(dbConnection, { - includes_tax: true, - region_id: region.id - }) + includesTaxShippingOption = await simpleShippingOptionFactory( + dbConnection, + { + includes_tax: true, + region_id: region.id, + } + ) } catch (err) { console.log(err) } }) - afterEach(async() => { + afterEach(async () => { const db = useDb() return await db.teardown() }) @@ -76,26 +78,27 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/orders", () => { const api = useApi() const orderWithShippingMethodRes = await api.post( - `/admin/orders/${order.id}/shipping-methods`, - { - option_id: includesTaxShippingOption.id, - price: 10, + `/admin/orders/${order.id}/shipping-methods`, + { + option_id: includesTaxShippingOption.id, + price: 10, + }, + { + headers: { + Authorization: "Bearer test_token", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + } ) expect(orderWithShippingMethodRes.status).toEqual(200) - expect(orderWithShippingMethodRes.data.order.shipping_methods) - .toEqual(expect.arrayContaining([ + expect(orderWithShippingMethodRes.data.order.shipping_methods).toEqual( + expect.arrayContaining([ expect.objectContaining({ shipping_option_id: includesTaxShippingOption.id, includes_tax: true, - }) - ])) + }), + ]) + ) }) }) }) diff --git a/integration-tests/api/__tests__/admin/order/order.js b/integration-tests/api/__tests__/admin/order/order.js index b947618642..02be314a2c 100644 --- a/integration-tests/api/__tests__/admin/order/order.js +++ b/integration-tests/api/__tests__/admin/order/order.js @@ -33,7 +33,7 @@ describe("/admin/orders", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/payment-collection.js b/integration-tests/api/__tests__/admin/payment-collection.js index aeb089c4cb..e99a656724 100644 --- a/integration-tests/api/__tests__/admin/payment-collection.js +++ b/integration-tests/api/__tests__/admin/payment-collection.js @@ -28,7 +28,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment-collections", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_ORDER_EDITING: true }, - verbose: true, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/payment.js b/integration-tests/api/__tests__/admin/payment.js index f8b58f4e66..291f7ae6a6 100644 --- a/integration-tests/api/__tests__/admin/payment.js +++ b/integration-tests/api/__tests__/admin/payment.js @@ -31,7 +31,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_ORDER_EDITING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index 7b3c1cc3f3..ed9ddee24a 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -1415,7 +1415,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/price-lists", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 4761699e1d..173035994f 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -36,7 +36,9 @@ describe("/admin/products", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ + cwd, + }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/publishable-api-key.js b/integration-tests/api/__tests__/admin/publishable-api-key.js index c1435d133a..9e1bbdb228 100644 --- a/integration-tests/api/__tests__/admin/publishable-api-key.js +++ b/integration-tests/api/__tests__/admin/publishable-api-key.js @@ -43,7 +43,6 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => { MEDUSA_FF_PUBLISHABLE_API_KEYS: true, MEDUSA_FF_SALES_CHANNELS: true, }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/region.js b/integration-tests/api/__tests__/admin/region.js index a700381fb0..741d42c6ed 100644 --- a/integration-tests/api/__tests__/admin/region.js +++ b/integration-tests/api/__tests__/admin/region.js @@ -2,11 +2,12 @@ const path = require("path") const { Region } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") -const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") -const { simpleRegionFactory } = require("../../factories"); +const { simpleRegionFactory } = require("../../factories") const adminReqConfig = { headers: { @@ -304,7 +305,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process @@ -364,7 +364,7 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => { name: "region-including-taxes", }) ) - }); + }) it("should allow to update a region that includes tax", async function () { const api = useApi() @@ -376,17 +376,19 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => { expect(response.data.region.includes_tax).toBe(false) - response = await api.post( - `/admin/regions/${region1TaxInclusiveId}`, - { - includes_tax: true, - }, - adminReqConfig, - ).catch((err) => { - console.log(err) - }) + response = await api + .post( + `/admin/regions/${region1TaxInclusiveId}`, + { + includes_tax: true, + }, + adminReqConfig + ) + .catch((err) => { + console.log(err) + }) expect(response.data.region.includes_tax).toBe(true) - }); + }) }) -}) \ No newline at end of file +}) diff --git a/integration-tests/api/__tests__/admin/return-reason.js b/integration-tests/api/__tests__/admin/return-reason.js index 84ca8519e5..917ea05e4e 100644 --- a/integration-tests/api/__tests__/admin/return-reason.js +++ b/integration-tests/api/__tests__/admin/return-reason.js @@ -15,7 +15,7 @@ describe("/admin/return-reasons", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 80b8955f83..e1d864fd22 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -34,7 +34,6 @@ describe("sales channels", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/shipping-options.js b/integration-tests/api/__tests__/admin/shipping-options.js index 92eaf630f6..dc077bf39a 100644 --- a/integration-tests/api/__tests__/admin/shipping-options.js +++ b/integration-tests/api/__tests__/admin/shipping-options.js @@ -1,15 +1,17 @@ const path = require("path") -const { - ShippingProfile, -} = require("@medusajs/medusa") +const { ShippingProfile } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") -const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const shippingOptionSeeder = require("../../helpers/shipping-option-seeder") -const { simpleShippingOptionFactory, simpleRegionFactory } = require("../../factories") +const { + simpleShippingOptionFactory, + simpleRegionFactory, +} = require("../../factories") const adminReqConfig = { headers: { @@ -475,7 +477,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process @@ -517,9 +518,12 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => { it("should creates a shipping option that includes tax", async () => { const api = useApi() - const defaultProfile = await dbConnection.manager.findOne(ShippingProfile, { - type: "default", - }) + const defaultProfile = await dbConnection.manager.findOne( + ShippingProfile, + { + type: "default", + } + ) const payload = { name: "Test option", @@ -551,7 +555,10 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => { const api = useApi() let response = await api - .get(`/admin/shipping-options/${shippingOptionIncludesTaxId}`, adminReqConfig) + .get( + `/admin/shipping-options/${shippingOptionIncludesTaxId}`, + adminReqConfig + ) .catch((err) => { console.log(err) }) @@ -573,7 +580,11 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => { } response = await api - .post(`/admin/shipping-options/${shippingOptionIncludesTaxId}`, payload, adminReqConfig) + .post( + `/admin/shipping-options/${shippingOptionIncludesTaxId}`, + payload, + adminReqConfig + ) .catch((err) => { console.log(err) }) diff --git a/integration-tests/api/__tests__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js index d3db302bbd..577bae9b74 100644 --- a/integration-tests/api/__tests__/admin/swaps.js +++ b/integration-tests/api/__tests__/admin/swaps.js @@ -363,7 +363,6 @@ describe("/admin/swaps", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/user.js b/integration-tests/api/__tests__/admin/user.js index aea7893564..5acaf2ef22 100644 --- a/integration-tests/api/__tests__/admin/user.js +++ b/integration-tests/api/__tests__/admin/user.js @@ -28,7 +28,7 @@ describe("/admin/users", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/batch-jobs/order/export.js b/integration-tests/api/__tests__/batch-jobs/order/export.js index 4c43a71046..5a818c4dad 100644 --- a/integration-tests/api/__tests__/batch-jobs/order/export.js +++ b/integration-tests/api/__tests__/batch-jobs/order/export.js @@ -1,6 +1,6 @@ const path = require("path") const fs = require("fs/promises") -import { sep, resolve } from "path" +import { resolve, sep } from "path" const setupServer = require("../../../../helpers/setup-server") const { useApi } = require("../../../../helpers/use-api") @@ -31,7 +31,6 @@ describe("Batchjob with type order-export", () => { cwd, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: false, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/price-list/import.js b/integration-tests/api/__tests__/batch-jobs/price-list/import.js index 90a91ca6f3..d209931668 100644 --- a/integration-tests/api/__tests__/batch-jobs/price-list/import.js +++ b/integration-tests/api/__tests__/batch-jobs/price-list/import.js @@ -66,7 +66,6 @@ describe("Price list import batch job", () => { cwd, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: false, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/product/export.js b/integration-tests/api/__tests__/batch-jobs/product/export.js index 6f568df5e3..87cf4bb776 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/export.js +++ b/integration-tests/api/__tests__/batch-jobs/product/export.js @@ -16,7 +16,7 @@ const adminReqConfig = { }, } -jest.setTimeout(100000000) +jest.setTimeout(180000) describe("Batch job of product-export type", () => { let medusaProcess @@ -31,7 +31,6 @@ describe("Batch job of product-export type", () => { cwd, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: false, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js index 50b900d3af..d0b2069f0e 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js +++ b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js @@ -61,7 +61,6 @@ describe("Product import - Sales Channel", () => { env: { MEDUSA_FF_SALES_CHANNELS: true }, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/batch-jobs/product/import.js b/integration-tests/api/__tests__/batch-jobs/product/import.js index 8d5483a07b..48ed2e8ee1 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/import.js +++ b/integration-tests/api/__tests__/batch-jobs/product/import.js @@ -59,7 +59,6 @@ describe("Product import batch job", () => { cwd, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: false, }) }) diff --git a/integration-tests/api/__tests__/line-item-adjustments/ff-sales-channel.js b/integration-tests/api/__tests__/line-item-adjustments/ff-sales-channel.js index cfd63a1f39..e3d85a809f 100644 --- a/integration-tests/api/__tests__/line-item-adjustments/ff-sales-channel.js +++ b/integration-tests/api/__tests__/line-item-adjustments/ff-sales-channel.js @@ -23,7 +23,6 @@ describe("Line Item - Sales Channel", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/price-selection/tax-inclusive-prices.js b/integration-tests/api/__tests__/price-selection/tax-inclusive-prices.js index 7c83bf7f9c..0f14358cf5 100644 --- a/integration-tests/api/__tests__/price-selection/tax-inclusive-prices.js +++ b/integration-tests/api/__tests__/price-selection/tax-inclusive-prices.js @@ -28,7 +28,6 @@ describe("tax inclusive prices", () => { const [process, conn] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = conn // await initDb({ cwd }) medusaProcess = process // await setupServer({ cwd }) diff --git a/integration-tests/api/__tests__/returns/ff-tax-inclusive-pricing.js b/integration-tests/api/__tests__/returns/ff-tax-inclusive-pricing.js index cdc9a1fb23..5b724a78ee 100644 --- a/integration-tests/api/__tests__/returns/ff-tax-inclusive-pricing.js +++ b/integration-tests/api/__tests__/returns/ff-tax-inclusive-pricing.js @@ -74,7 +74,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 061befb0ef..ceca343ab2 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -47,7 +47,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/cart/ff-sales-channels.js b/integration-tests/api/__tests__/store/cart/ff-sales-channels.js index 9ae859af8a..367374723e 100644 --- a/integration-tests/api/__tests__/store/cart/ff-sales-channels.js +++ b/integration-tests/api/__tests__/store/cart/ff-sales-channels.js @@ -28,7 +28,6 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/store/cart/ff-tax-inclusive-pricing.js b/integration-tests/api/__tests__/store/cart/ff-tax-inclusive-pricing.js index 2ab306025a..9a8432a6c8 100644 --- a/integration-tests/api/__tests__/store/cart/ff-tax-inclusive-pricing.js +++ b/integration-tests/api/__tests__/store/cart/ff-tax-inclusive-pricing.js @@ -27,7 +27,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process @@ -261,6 +260,25 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => { }) describe("with a cart with full tax inclusive variant pricing", () => { + const variantId1 = IdMap.getId("test-variant-1-tax-inclusive") + const variantId2 = IdMap.getId("test-variant-2-tax-inclusive") + const productId1 = IdMap.getId("test-product-1-tax-inclusive") + const productId2 = IdMap.getId("test-product-2-tax-inclusive") + + const createCartPayload = { + region_id: regionId, + items: [ + { + variant_id: variantId1, + quantity: 1, + }, + { + variant_id: variantId2, + quantity: 1, + }, + ], + } + beforeEach(async () => { await simpleRegionFactory(dbConnection, regionData) await simpleProductFactory( @@ -319,6 +337,25 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => { }) describe("with a cart mixing tax inclusive and exclusive variant pricing", () => { + const variantId1 = IdMap.getId("test-variant-1-mixed-tax-inclusive") + const variantId2 = IdMap.getId("test-variant-2-mixed-tax-inclusive") + const productId1 = IdMap.getId("test-product-1-mixed-tax-inclusive") + const productId2 = IdMap.getId("test-product-2-mixed-tax-inclusive") + + const createCartPayload = { + region_id: regionId, + items: [ + { + variant_id: variantId1, + quantity: 1, + }, + { + variant_id: variantId2, + quantity: 1, + }, + ], + } + beforeEach(async () => { await simpleRegionFactory(dbConnection, regionData) await simpleProductFactory( diff --git a/integration-tests/api/__tests__/store/collections.js b/integration-tests/api/__tests__/store/collections.js index 328b151d27..a630f6aca7 100644 --- a/integration-tests/api/__tests__/store/collections.js +++ b/integration-tests/api/__tests__/store/collections.js @@ -14,7 +14,7 @@ describe("/store/collections", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: true }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index f9d3137b42..c85bff0855 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -19,7 +19,7 @@ describe("/store/customers", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js index 23474d2965..6dc3c95cf1 100644 --- a/integration-tests/api/__tests__/store/order-edit.js +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -30,7 +30,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_ORDER_EDITING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index c32a1b288d..b01b1bf376 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -31,7 +31,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/payment-collection.js b/integration-tests/api/__tests__/store/payment-collection.js index 8beeb968d3..47fe19a709 100644 --- a/integration-tests/api/__tests__/store/payment-collection.js +++ b/integration-tests/api/__tests__/store/payment-collection.js @@ -25,7 +25,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_ORDER_EDITING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/store/product-variants.js b/integration-tests/api/__tests__/store/product-variants.js index bb7ffc408b..efdce8b785 100644 --- a/integration-tests/api/__tests__/store/product-variants.js +++ b/integration-tests/api/__tests__/store/product-variants.js @@ -15,7 +15,7 @@ describe("/store/variants", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/sales-channels.js b/integration-tests/api/__tests__/store/sales-channels.js index 69078e01e8..5e67f85054 100644 --- a/integration-tests/api/__tests__/store/sales-channels.js +++ b/integration-tests/api/__tests__/store/sales-channels.js @@ -31,7 +31,6 @@ describe("sales channels", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/taxes/automatic-taxes.js b/integration-tests/api/__tests__/taxes/automatic-taxes.js index d1c7501bc6..bf1427f8ea 100644 --- a/integration-tests/api/__tests__/taxes/automatic-taxes.js +++ b/integration-tests/api/__tests__/taxes/automatic-taxes.js @@ -28,7 +28,7 @@ describe("Automatic Cart Taxes", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/taxes/manual-taxes.js b/integration-tests/api/__tests__/taxes/manual-taxes.js index 3de5fee0d9..4ae9a017d7 100644 --- a/integration-tests/api/__tests__/taxes/manual-taxes.js +++ b/integration-tests/api/__tests__/taxes/manual-taxes.js @@ -25,7 +25,7 @@ describe("Manual Cart Taxes", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/taxes/orders/ff-tax-inclusive-pricing.js b/integration-tests/api/__tests__/taxes/orders/ff-tax-inclusive-pricing.js index addddc5177..09cd79ee14 100644 --- a/integration-tests/api/__tests__/taxes/orders/ff-tax-inclusive-pricing.js +++ b/integration-tests/api/__tests__/taxes/orders/ff-tax-inclusive-pricing.js @@ -22,7 +22,6 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING]: Order Taxes", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/factories/simple-price-list-factory.ts b/integration-tests/api/factories/simple-price-list-factory.ts index c1f1d30dfc..a9cc0693ac 100644 --- a/integration-tests/api/factories/simple-price-list-factory.ts +++ b/integration-tests/api/factories/simple-price-list-factory.ts @@ -1,8 +1,9 @@ import { - PriceList, + CustomerGroup, MoneyAmount, - PriceListType, + PriceList, PriceListStatus, + PriceListType, } from "@medusajs/medusa" import faker from "faker" import { Connection } from "typeorm" @@ -41,7 +42,7 @@ export const simplePriceListFactory = async ( const listId = data.id || `simple-price-list-${Math.random() * 1000}` - let customerGroups = [] + let customerGroups: CustomerGroup[] = [] if (typeof data.customer_groups !== "undefined") { customerGroups = await Promise.all( data.customer_groups.map((group) => diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index e654bc8fdb..d6300594ba 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,16 +8,16 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.6.5-dev-1669708431707", + "@medusajs/medusa": "1.6.5-dev-1670340951307", "faker": "^5.5.3", - "medusa-interfaces": "1.3.3-dev-1669708431707", + "medusa-interfaces": "1.3.3-dev-1670340951307", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.19-dev-1669708431707", + "babel-preset-medusa-package": "1.1.19-dev-1670340951307", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index b040551929..7643b80159 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1775,9 +1775,9 @@ __metadata: languageName: node linkType: hard -"@medusajs/medusa-cli@npm:1.3.5-dev-1669708431707": - version: 1.3.5-dev-1669708431707 - resolution: "@medusajs/medusa-cli@npm:1.3.5-dev-1669708431707" +"@medusajs/medusa-cli@npm:1.3.5-dev-1670340951307": + version: 1.3.5-dev-1670340951307 + resolution: "@medusajs/medusa-cli@npm:1.3.5-dev-1670340951307" dependencies: "@babel/polyfill": ^7.8.7 "@babel/runtime": ^7.9.6 @@ -1793,8 +1793,8 @@ __metadata: inquirer: ^8.0.0 is-valid-path: ^0.1.1 meant: ^1.0.1 - medusa-core-utils: 1.1.35-dev-1669708431707 - medusa-telemetry: 0.0.15-dev-1669708431707 + medusa-core-utils: 1.1.35-dev-1670340951307 + medusa-telemetry: 0.0.15-dev-1670340951307 netrc-parser: ^3.1.6 open: ^8.0.6 ora: ^5.4.1 @@ -1809,15 +1809,15 @@ __metadata: yargs: ^15.3.1 bin: medusa: cli.js - checksum: 779d98b21775542534466272d8fe415620024846aba435b45ae9956eab3fc7c627f85540ac0ac7a5282a0ab15d2c04c2dd99e72ca071b46f55da20f34effb69a + checksum: 7588d1b5ef91b720c36379db278da61b308403f30807d4c470418f1072203238b2820ce14d8b8ddf154b7f61f6ae79afc081ad9e481311e83cfc44546026ba01 languageName: node linkType: hard -"@medusajs/medusa@npm:1.6.5-dev-1669708431707": - version: 1.6.5-dev-1669708431707 - resolution: "@medusajs/medusa@npm:1.6.5-dev-1669708431707" +"@medusajs/medusa@npm:1.6.5-dev-1670340951307": + version: 1.6.5-dev-1670340951307 + resolution: "@medusajs/medusa@npm:1.6.5-dev-1670340951307" dependencies: - "@medusajs/medusa-cli": 1.3.5-dev-1669708431707 + "@medusajs/medusa-cli": 1.3.5-dev-1670340951307 "@types/ioredis": ^4.28.10 "@types/lodash": ^4.14.168 awilix: ^8.0.0 @@ -1839,8 +1839,8 @@ __metadata: ioredis-mock: ^5.6.0 iso8601-duration: ^1.3.0 jsonwebtoken: ^8.5.1 - medusa-core-utils: 1.1.35-dev-1669708431707 - medusa-test-utils: 1.1.37-dev-1669708431707 + medusa-core-utils: 1.1.35-dev-1670340951307 + medusa-test-utils: 1.1.37-dev-1670340951307 morgan: ^1.9.1 multer: ^1.4.2 node-schedule: ^2.1.0 @@ -1865,7 +1865,7 @@ __metadata: typeorm: 0.2.x bin: medusa: cli.js - checksum: 7a7ec5ba7971112e74652791cff5eb8bfde640158618b300289d67bd753859c8312256fb2aa93f3523d2a4399f6d8b6c106e03e253f9a9518405b1224043299d + checksum: 274637b6e19db96f5cc561837b254bf2f5a1fea62ecc5258739413b48e568d7b3bdd0d486b32009d7e3ee27d684bbd3451943e6a42d092624882331139befee3 languageName: node linkType: hard @@ -2446,11 +2446,11 @@ __metadata: "@babel/cli": ^7.12.10 "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 - "@medusajs/medusa": 1.6.5-dev-1669708431707 - babel-preset-medusa-package: 1.1.19-dev-1669708431707 + "@medusajs/medusa": 1.6.5-dev-1670340951307 + babel-preset-medusa-package: 1.1.19-dev-1670340951307 faker: ^5.5.3 jest: ^26.6.3 - medusa-interfaces: 1.3.3-dev-1669708431707 + medusa-interfaces: 1.3.3-dev-1670340951307 typeorm: ^0.2.31 languageName: unknown linkType: soft @@ -2757,9 +2757,9 @@ __metadata: languageName: node linkType: hard -"babel-preset-medusa-package@npm:1.1.19-dev-1669708431707": - version: 1.1.19-dev-1669708431707 - resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1669708431707" +"babel-preset-medusa-package@npm:1.1.19-dev-1670340951307": + version: 1.1.19-dev-1670340951307 + resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1670340951307" dependencies: "@babel/plugin-proposal-class-properties": ^7.12.1 "@babel/plugin-proposal-decorators": ^7.12.1 @@ -2773,7 +2773,7 @@ __metadata: core-js: ^3.7.0 peerDependencies: "@babel/core": ^7.11.6 - checksum: 2b01b0754da0a4bec26abcb6c94d91d7c2fd06bf9d58c23dac9266dc8c7cb470a6a8874d1564af84b068684d34028fb0288c7eae5f271a16cd1570ccaf1aa413 + checksum: b091e66d23c22e26a55f8039810f73910a3860af5838904aada35e6066d3d1f9c85e5897e2879388452fd424663e676a4e19ad960167a1d001105e2d797715bc languageName: node linkType: hard @@ -6919,29 +6919,29 @@ __metadata: languageName: node linkType: hard -"medusa-core-utils@npm:1.1.35-dev-1669708431707": - version: 1.1.35-dev-1669708431707 - resolution: "medusa-core-utils@npm:1.1.35-dev-1669708431707" +"medusa-core-utils@npm:1.1.35-dev-1670340951307": + version: 1.1.35-dev-1670340951307 + resolution: "medusa-core-utils@npm:1.1.35-dev-1670340951307" dependencies: joi: ^17.3.0 joi-objectid: ^3.0.1 - checksum: ac797ee8b9a165a6e90e11fbe9312bcfcaaa4271a9ef79b2cb659b053697cbee80580b3aae9bead7e2b738a864df30f150b01d9598fceb8262d6d11496a68ab4 + checksum: 9304c0426dec41f33973dd122491bd3025a65eabc94616de7f63b492b558e71234fc0e02af3a13cea014b8a4bead91053e0416e107cc0b74657af9782d5c6023 languageName: node linkType: hard -"medusa-interfaces@npm:1.3.3-dev-1669708431707": - version: 1.3.3-dev-1669708431707 - resolution: "medusa-interfaces@npm:1.3.3-dev-1669708431707" +"medusa-interfaces@npm:1.3.3-dev-1670340951307": + version: 1.3.3-dev-1670340951307 + resolution: "medusa-interfaces@npm:1.3.3-dev-1670340951307" peerDependencies: medusa-core-utils: ^1.1.31 typeorm: 0.x - checksum: edad068df3783072f178cac3adfa646e8886a55bf07409addec4ab18eab8f8e09e9d5ac34c1e06c65cd111330f003325c72f9dc8585348d20382a1dacf3d3536 + checksum: 9eda49c0cb3922f3e1109ad231389b54ad49f730444108fd10876beab9f7845c47b965dcfe32e7169a9a4845edd143cfad96187c5a5814cbd05f45852d56c130 languageName: node linkType: hard -"medusa-telemetry@npm:0.0.15-dev-1669708431707": - version: 0.0.15-dev-1669708431707 - resolution: "medusa-telemetry@npm:0.0.15-dev-1669708431707" +"medusa-telemetry@npm:0.0.15-dev-1670340951307": + version: 0.0.15-dev-1670340951307 + resolution: "medusa-telemetry@npm:0.0.15-dev-1670340951307" dependencies: axios: ^0.21.1 axios-retry: ^3.1.9 @@ -6952,18 +6952,18 @@ __metadata: is-docker: ^2.2.1 remove-trailing-slash: ^0.1.1 uuid: ^8.3.2 - checksum: 0116c6d4d70811290ba423868cbd5fc8600cf66c81942c0fb69eab41910e783f6f90b8d401e95f2847e4aa0fc74dbcd5115e30cd9758be2f01b4577d934fcb2c + checksum: 15f3b7d1d639b3024d964792455a13f820fc596d7f01583f44de9803d44f129e39b201a00dab94dc43b67f31b4852835e96fd58363b55a71d6979856be58df75 languageName: node linkType: hard -"medusa-test-utils@npm:1.1.37-dev-1669708431707": - version: 1.1.37-dev-1669708431707 - resolution: "medusa-test-utils@npm:1.1.37-dev-1669708431707" +"medusa-test-utils@npm:1.1.37-dev-1670340951307": + version: 1.1.37-dev-1670340951307 + resolution: "medusa-test-utils@npm:1.1.37-dev-1670340951307" dependencies: "@babel/plugin-transform-classes": ^7.9.5 - medusa-core-utils: 1.1.35-dev-1669708431707 + medusa-core-utils: 1.1.35-dev-1670340951307 randomatic: ^3.1.1 - checksum: b89c99be68369aae6f72c395eaec11f06c64415ff6b1e9a8616fd2e14e68a1f3cfb58e7722f48057c0da7da5d1dcb260ecaa49bd89c241a55d38767b2307600b + checksum: 8e96932f28e402c9561f6ea6c13a5d5ac8b5b83dc38a5e41d4ad5bcac73aa35886fe3b6d55809692594b468fc7be6e89477b6cd3d694be23f6a5abac7cb61c62 languageName: node linkType: hard diff --git a/integration-tests/helpers/setup-server.js b/integration-tests/helpers/setup-server.js index 3e9f2cf232..d2e3c24dbd 100644 --- a/integration-tests/helpers/setup-server.js +++ b/integration-tests/helpers/setup-server.js @@ -11,6 +11,8 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => { const workerId = parseInt(process.env.JEST_WORKER_ID || "1") const redisUrlWithDatabase = `${redisUrl}/${workerId - 1}` + verbose = verbose ?? false + return new Promise((resolve, reject) => { const medusaProcess = spawn("node", [path.resolve(serverPath)], { cwd, @@ -21,6 +23,7 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => { COOKIE_SECRET: "test", REDIS_URL: redisUrl ? redisUrlWithDatabase : undefined, // If provided, will use a real instance, otherwise a fake instance UPLOAD_DIR: uploadDir, // If provided, will be used for the fake local file service + CACHE_TTL: 0, // By default the cache service is disabled and 0 means that none of the cache key/value will be stored. ...env, }, stdio: verbose diff --git a/package.json b/package.json index f2d43cc458..c380bd3ef1 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "jest": "jest", "test": "turbo run test", "test:integration": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js", - "test:integration:api": "NODE_ENV=test jest --detectOpenHandles --forceExit --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/api", + "test:integration:api": "NODE_ENV=test jest --detectOpenHandles --forceExit --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/api -- integration-tests/api/__tests__/store/cart/cart.js", "test:integration:plugins": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/plugins", "test:fixtures": "NODE_ENV=test jest --config=docs-util/jest.config.js --runInBand --bail", "openapi:generate": "node ./scripts/build-openapi.js", diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js index 9977a9f397..2d04beb96a 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js @@ -98,19 +98,24 @@ describe("POST /store/carts", () => { }) it("calls line item generate", () => { + expect(CartServiceMock.addOrUpdateLineItems).toHaveBeenCalledTimes(1) + expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - IdMap.getId("testRegion"), - 3, - { customer_id: undefined } + [ + { + variantId: IdMap.getId("testVariant"), + quantity: 3, + }, + { + variantId: IdMap.getId("testVariant1"), + quantity: 1, + }, + ], + { + region_id: IdMap.getId("testRegion"), + customer_id: undefined, + } ) - expect(LineItemServiceMock.generate).toHaveBeenCalledWith( - IdMap.getId("testVariant1"), - IdMap.getId("testRegion"), - 1, - { customer_id: undefined } - ) - expect(CartServiceMock.addLineItem).toHaveBeenCalledTimes(2) }) it("returns cart", () => { diff --git a/packages/medusa/src/api/routes/store/carts/create-cart.ts b/packages/medusa/src/api/routes/store/carts/create-cart.ts index d78c940f91..cbeb8a26f7 100644 --- a/packages/medusa/src/api/routes/store/carts/create-cart.ts +++ b/packages/medusa/src/api/routes/store/carts/create-cart.ts @@ -17,7 +17,7 @@ import { RegionService, } from "../../../../services" import { defaultStoreCartFields, defaultStoreCartRelations } from "." -import { Cart } from "../../../../models" +import { Cart, LineItem } from "../../../../models" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { FlagRouter } from "../../../../utils/flag-router" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" @@ -179,24 +179,30 @@ export default async (req, res) => { let cart: Cart await entityManager.transaction(async (manager) => { - cart = await cartService.withTransaction(manager).create(toCreate) + const cartServiceTx = cartService.withTransaction(manager) + const lineItemServiceTx = lineItemService.withTransaction(manager) - if (validated.items) { - await Promise.all( - validated.items.map(async (i) => { - const lineItem = await lineItemService - .withTransaction(manager) - .generate(i.variant_id, regionId, i.quantity, { - customer_id: req.user?.customer_id, - }) - return await cartService - .withTransaction(manager) - .addLineItem(cart.id, lineItem, { - validateSalesChannels: - featureFlagRouter.isFeatureEnabled("sales_channels"), - }) - }) + cart = await cartServiceTx.create(toCreate) + + if (validated.items?.length) { + const generateInputData = validated.items.map((item) => { + return { + variantId: item.variant_id, + quantity: item.quantity, + } + }) + const generatedLineItems: LineItem[] = await lineItemServiceTx.generate( + generateInputData, + { + region_id: regionId, + customer_id: req.user?.customer_id, + } ) + + await cartServiceTx.addOrUpdateLineItems(cart.id, generatedLineItems, { + validateSalesChannels: + featureFlagRouter.isFeatureEnabled("sales_channels"), + }) } }) diff --git a/packages/medusa/src/interfaces/index.ts b/packages/medusa/src/interfaces/index.ts index 38c2dfccc7..d6984c55dc 100644 --- a/packages/medusa/src/interfaces/index.ts +++ b/packages/medusa/src/interfaces/index.ts @@ -10,3 +10,4 @@ export * from "./models/base-entity" export * from "./models/soft-deletable-entity" export * from "./search-service" export * from "./payment-service" +export * from "./services" diff --git a/packages/medusa/src/interfaces/services/cache.ts b/packages/medusa/src/interfaces/services/cache.ts new file mode 100644 index 0000000000..c96cd6a306 --- /dev/null +++ b/packages/medusa/src/interfaces/services/cache.ts @@ -0,0 +1,7 @@ +export interface ICacheService { + get(key: string): Promise + + set(key: string, data: unknown, ttl?: number): Promise + + invalidate(key: string): Promise +} diff --git a/packages/medusa/src/interfaces/services/index.ts b/packages/medusa/src/interfaces/services/index.ts new file mode 100644 index 0000000000..202c17d92e --- /dev/null +++ b/packages/medusa/src/interfaces/services/index.ts @@ -0,0 +1 @@ +export * from "./cache" diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 87a67af446..72f803b6ee 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -172,11 +172,19 @@ export class MoneyAmountRepository extends Repository { } if (region_id || currency_code) { qb.andWhere( - new Brackets((qb) => - qb - .where({ region_id: region_id }) - .orWhere({ currency_code: currency_code }) - ) + new Brackets((qb) => { + if (region_id && !currency_code) { + qb.where({ region_id: region_id }) + } + if (!region_id && currency_code) { + qb.where({ currency_code: currency_code }) + } + if (currency_code && region_id) { + qb.where({ region_id: region_id }).orWhere({ + currency_code: currency_code, + }) + } + }) ) } else if (!customer_id && !include_discount_prices) { qb.andWhere("price_list.id IS null") diff --git a/packages/medusa/src/services/__mocks__/cache.js b/packages/medusa/src/services/__mocks__/cache.js new file mode 100644 index 0000000000..359bccef67 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/cache.js @@ -0,0 +1,11 @@ +export const cacheServiceMock = { + set: jest.fn().mockImplementation(async () => void 0), + get: jest.fn().mockImplementation(async () => null), + invalidate: jest.fn().mockImplementation(async () => void 0), +} + +const mock = jest.fn().mockImplementation(() => { + return cacheServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__mocks__/cart.js b/packages/medusa/src/services/__mocks__/cart.js index e56880b1ef..f6ed6a1549 100644 --- a/packages/medusa/src/services/__mocks__/cart.js +++ b/packages/medusa/src/services/__mocks__/cart.js @@ -319,6 +319,9 @@ export const CartServiceMock = { addLineItem: jest.fn().mockImplementation((cartId, lineItem) => { return Promise.resolve() }), + addOrUpdateLineItems: jest.fn().mockImplementation((cartId, lineItem) => { + return Promise.resolve() + }), setPaymentMethod: jest.fn().mockImplementation((cartId, method) => { if (method.provider_id === "default_provider") { return Promise.resolve(carts.cartWithPaySessions) diff --git a/packages/medusa/src/services/__tests__/discount.js b/packages/medusa/src/services/__tests__/discount.js index 8a3f5fbca6..b820ca7590 100644 --- a/packages/medusa/src/services/__tests__/discount.js +++ b/packages/medusa/src/services/__tests__/discount.js @@ -1,6 +1,8 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { FlagRouter } from "../../utils/flag-router" import DiscountService from "../discount" +import { TotalsServiceMock } from "../__mocks__/totals" +import { newTotalsServiceMock } from "../__mocks__/new-totals" const featureFlagRouter = new FlagRouter({}) @@ -601,15 +603,28 @@ describe("DiscountService", () => { }) const totalsService = { - getSubtotal: () => { + ...TotalsServiceMock, + getSubtotal: async () => { return 1100 }, } + const newTotalsService = { + ...newTotalsServiceMock, + getLineItemTotals: async () => { + return [ + { + subtotal: 1100, + }, + ] + }, + } + const discountService = new DiscountService({ manager: MockManager, discountRepository, totalsService, + newTotalsService, featureFlagRouter, }) @@ -631,21 +646,31 @@ describe("DiscountService", () => { }) it("correctly calculates fixed + total discount", async () => { + let item = { + unit_price: 400, + quantity: 2, + allow_discounts: true, + } + const adjustment1 = await discountService.calculateDiscountForLineItem( "disc_fixed_total", + item, { - unit_price: 400, - quantity: 2, - allow_discounts: true, + items: [item], } ) + item = { + unit_price: 300, + quantity: 1, + allow_discounts: true, + } + const adjustment2 = await discountService.calculateDiscountForLineItem( "disc_fixed_total", + item, { - unit_price: 300, - quantity: 1, - allow_discounts: true, + items: [item], } ) diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 76dbdcccc3..19a0def6f3 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -32,9 +32,7 @@ import { RegionServiceMock } from "../__mocks__/region" } const productVariantService = { - withTransaction: function () { - return this - }, + ...ProductVariantServiceMock, retrieve: (query) => { if (query === IdMap.getId("test-giftcard")) { return { @@ -58,15 +56,25 @@ import { RegionServiceMock } from "../__mocks__/region" } }, getRegionPrice: () => 100, + list: jest.fn().mockImplementation(async (selector) => { + return (selector.id || []).map((id) => ({ + id, + title: "Test variant", + product: { + title: "Test product", + thumbnail: "", + discountable: false, + is_giftcard: true, + }, + })) + }), } const pricingService = { - withTransaction: function () { - return this - }, - getProductVariantPricingById: () => { + ...PricingServiceMock, + getProductVariantsPricing: () => { return { - calculated_price: 100, + [IdMap.getId("test-giftcard")]: { calculated_price: 100 }, } }, getProductVariantPricing: () => { @@ -106,15 +114,17 @@ import { RegionServiceMock } from "../__mocks__/region" }) expect(lineItemRepository.create).toHaveBeenCalledTimes(1) - expect(lineItemRepository.create).toHaveBeenCalledWith({ - variant_id: IdMap.getId("test-variant"), - cart_id: IdMap.getId("test-cart"), - title: "Test product", - description: "Test variant", - thumbnail: "", - unit_price: 100, - quantity: 1, - }) + expect(lineItemRepository.create).toHaveBeenCalledWith([ + { + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 100, + quantity: 1, + }, + ]) }) it("successfully create a line item with price and quantity", async () => { @@ -126,12 +136,14 @@ import { RegionServiceMock } from "../__mocks__/region" }) expect(lineItemRepository.create).toHaveBeenCalledTimes(1) - expect(lineItemRepository.create).toHaveBeenCalledWith({ - variant_id: IdMap.getId("test-variant"), - cart_id: IdMap.getId("test-cart"), - unit_price: 50, - quantity: 2, - }) + expect(lineItemRepository.create).toHaveBeenCalledWith([ + { + variant_id: IdMap.getId("test-variant"), + cart_id: IdMap.getId("test-cart"), + unit_price: 50, + quantity: 2, + }, + ]) }) it("successfully create a line item giftcard", async () => { @@ -147,8 +159,7 @@ import { RegionServiceMock } from "../__mocks__/region" }) expect(lineItemRepository.create).toHaveBeenCalledTimes(2) - expect(lineItemRepository.create).toHaveBeenNthCalledWith( - 2, + expect(lineItemRepository.create).toHaveBeenNthCalledWith(2, [ expect.objectContaining({ allow_discounts: false, variant_id: IdMap.getId("test-giftcard"), @@ -161,8 +172,8 @@ import { RegionServiceMock } from "../__mocks__/region" is_giftcard: true, should_merge: true, metadata: {}, - }) - ) + }), + ]) }) }) @@ -190,6 +201,7 @@ import { RegionServiceMock } from "../__mocks__/region" const lineItemService = new LineItemService({ manager: MockManager, lineItemRepository, + productVariantService: ProductVariantServiceMock, }) beforeEach(async () => { @@ -329,9 +341,7 @@ describe("LineItemService", () => { } const productVariantService = { - withTransaction: function () { - return this - }, + ...ProductVariantServiceMock, retrieve: (query) => { if (query === IdMap.getId("test-giftcard")) { return { @@ -355,16 +365,26 @@ describe("LineItemService", () => { } }, getRegionPrice: () => 100, + list: jest.fn().mockImplementation(async (selector) => { + return (selector.id || []).map((id) => ({ + id, + title: "Test variant", + product: { + title: "Test product", + thumbnail: "", + }, + })) + }), } const pricingService = { - withTransaction: function () { - return this - }, - getProductVariantPricingById: () => { + ...PricingServiceMock, + getProductVariantsPricing: () => { return { - calculated_price: 100, - calculated_price_includes_tax: true, + [IdMap.getId("test-variant")]: { + calculated_price: 100, + calculated_price_includes_tax: true, + }, } }, getProductVariantPricing: () => { @@ -423,6 +443,41 @@ describe("LineItemService", () => { }), }) }) + + it("successfully create a line item with tax inclusive set to true by passing an object", async () => { + await lineItemService.generate( + { + variantId: IdMap.getId("test-variant"), + quantity: 1, + }, + { + region_id: IdMap.getId("test-region"), + } + ) + + 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, + variant: expect.objectContaining({ + id: expect.any(String), + product: expect.objectContaining({ + thumbnail: "", + title: "Test product", + }), + title: "Test variant", + }), + }) + }) }) describe("generate", () => { const lineItemRepository = MockRepository({ @@ -448,9 +503,7 @@ describe("LineItemService", () => { } const productVariantService = { - withTransaction: function () { - return this - }, + ...ProductVariantServiceMock, retrieve: (query) => { if (query === IdMap.getId("test-giftcard")) { return { @@ -464,26 +517,30 @@ describe("LineItemService", () => { }, } } - return { - id: IdMap.getId("test-variant"), - title: "Test variant", - product: { - title: "Test product", - thumbnail: "", - }, - } }, getRegionPrice: () => 100, + list: jest.fn().mockImplementation(async (selector) => { + return (selector.id || []).map((id) => { + return { + id, + title: "Test variant", + product: { + title: "Test product", + thumbnail: "", + }, + } + }) + }), } const pricingService = { - withTransaction: function () { - return this - }, - getProductVariantPricingById: () => { + ...PricingServiceMock, + getProductVariantsPricing: () => { return { - calculated_price: 100, - calculated_price_includes_tax: false, + [IdMap.getId("test-variant")]: { + calculated_price: 100, + calculated_price_includes_tax: false, + }, } }, getProductVariantPricing: () => { @@ -542,6 +599,41 @@ describe("LineItemService", () => { }), }) }) + + it("successfully create a line item with tax inclusive set to false by passing an object", async () => { + await lineItemService.generate( + { + variantId: IdMap.getId("test-variant"), + quantity: 1, + }, + { + region_id: IdMap.getId("test-region"), + } + ) + + 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, + variant: expect.objectContaining({ + id: expect.any(String), + product: expect.objectContaining({ + thumbnail: "", + title: "Test product", + }), + title: "Test variant", + }), + }) + }) }) describe("clone", () => { diff --git a/packages/medusa/src/services/cache.ts b/packages/medusa/src/services/cache.ts new file mode 100644 index 0000000000..ec0b1c8df3 --- /dev/null +++ b/packages/medusa/src/services/cache.ts @@ -0,0 +1,67 @@ +import { Redis } from "ioredis" +import { ICacheService } from "../interfaces" + +const DEFAULT_CACHE_TIME = 30 // 30 seconds +const EXPIRY_MODE = "EX" // "EX" stands for an expiry time in second + +export default class CacheService implements ICacheService { + protected readonly redis_: Redis + + constructor({ redisClient }) { + this.redis_ = redisClient + } + + /** + * Set a key/value pair to the cache. + * It is also possible to manage the ttl through environment variable using CACHE_TTL. If the ttl is 0 it will + * act like the value should not be cached at all. + * @param key + * @param data + * @param ttl + */ + async set( + key: string, + data: Record, + ttl: number = DEFAULT_CACHE_TIME + ): Promise { + ttl = Number(process.env.CACHE_TTL ?? ttl) + if (ttl === 0) { + // No need to call redis set without expiry time + return + } + + await this.redis_.set(key, JSON.stringify(data), EXPIRY_MODE, ttl) + } + + /** + * Retrieve a cached value belonging to the given key. + * @param cacheKey + */ + async get(cacheKey: string): Promise { + try { + const cached = await this.redis_.get(cacheKey) + if (cached) { + return JSON.parse(cached) + } + } catch (err) { + await this.redis_.del(cacheKey) + } + return null + } + + /** + * Invalidate cache for a specific key. a key can be either a specific key or more global such as "ps:*". + * @param key + */ + async invalidate(key: string): Promise { + await this.redis_.del() + const keys = await this.redis_.keys(key) + const pipeline = this.redis_.pipeline() + + keys.forEach(function (key) { + pipeline.del(key) + }) + + await pipeline.exec() + } +} diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 600aa99676..ac6de30573 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -26,6 +26,7 @@ import { FilterableCartProps, isCart, LineItemUpdate, + LineItemValidateData, } from "../types/cart" import { AddressPayload, @@ -331,7 +332,7 @@ class CartService extends TransactionBaseService { rawCart.email = customer.email } - if (!data.region_id) { + if (!data.region_id && !data.region) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `A region_id must be provided when creating a cart` @@ -339,11 +340,13 @@ class CartService extends TransactionBaseService { } rawCart.region_id = data.region_id - const region = await this.regionService_ - .withTransaction(transactionManager) - .retrieve(data.region_id, { - relations: ["countries"], - }) + const region = data.region + ? data.region + : await this.regionService_ + .withTransaction(transactionManager) + .retrieve(data.region_id!, { + relations: ["countries"], + }) const regCountries = region.countries.map(({ iso_2 }) => iso_2) if (!data.shipping_address && !data.shipping_address_id) { @@ -555,15 +558,17 @@ class CartService extends TransactionBaseService { */ protected async validateLineItem( { sales_channel_id }: { sales_channel_id: string | null }, - lineItem: LineItem + lineItem: LineItemValidateData ): Promise { if (!sales_channel_id) { return true } - const lineItemVariant = await this.productVariantService_ - .withTransaction(this.manager_) - .retrieve(lineItem.variant_id) + const lineItemVariant = lineItem.variant?.product_id + ? lineItem.variant + : await this.productVariantService_ + .withTransaction(this.manager_) + .retrieve(lineItem.variant_id, { select: ["id", "product_id"] }) return !!( await this.productService_ @@ -583,6 +588,7 @@ class CartService extends TransactionBaseService { * validateSalesChannels - should check if product belongs to the same sales chanel as cart * (if cart has associated sales channel) * @return the result of the update operation + * @deprecated Use {@link addOrUpdateLineItems} instead. */ async addLineItem( cartId: string, @@ -659,7 +665,166 @@ class CartService extends TransactionBaseService { { cart_id: cartId, has_shipping: true }, { has_shipping: false } ) - .catch(() => void 0) + .catch((err: Error | MedusaError) => { + // We only want to catch the errors related to not found items since we don't care if there is not item to update + if ("type" in err && err.type === MedusaError.Types.NOT_FOUND) { + return + } + throw err + }) + + cart = await this.retrieve(cart.id, { + relations: ["items", "discounts", "discounts.rule", "region"], + }) + + await this.refreshAdjustments_(cart) + + await this.eventBus_ + .withTransaction(transactionManager) + .emit(CartService.Events.UPDATED, { id: cart.id }) + } + ) + } + + /** + * Adds or update one or multiple line items to the cart. It also update all existing items in the cart + * to have has_shipping to false. Finally, the adjustments will be updated. + * @param cartId - the id of the cart that we will add to + * @param lineItems - the line items to add. + * @param config + * validateSalesChannels - should check if product belongs to the same sales chanel as cart + * (if cart has associated sales channel) + * @return the result of the update operation + */ + async addOrUpdateLineItems( + cartId: string, + lineItems: LineItem | LineItem[], + config = { validateSalesChannels: true } + ): Promise { + const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems] + + const select: (keyof Cart)[] = ["id"] + + if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { + select.push("sales_channel_id") + } + + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + let cart = await this.retrieve(cartId, { select }) + + if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { + if (config.validateSalesChannels) { + const areValid = await Promise.all( + items.map(async (item) => { + return await this.validateLineItem(cart, item) + }) + ) + + const invalidProducts = areValid + .map((valid, index) => { + return !valid ? { title: items[index].title } : undefined + }) + .filter((v): v is { title: string } => !!v) + + if (invalidProducts.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The products [${invalidProducts + .map((item) => item.title) + .join( + " - " + )}] must belongs to the sales channel on which the cart has been created.` + ) + } + } + } + + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + const inventoryServiceTx = + this.inventoryService_.withTransaction(transactionManager) + + const existingItems = await lineItemServiceTx.list( + { + cart_id: cart.id, + variant_id: In([items.map((item) => item.variant_id)]), + should_merge: true, + }, + { select: ["id", "metadata", "quantity"] } + ) + + const existingItemsVariantMap = new Map() + existingItems.forEach((item) => { + existingItemsVariantMap.set(item.variant_id, item) + }) + + const lineItemsToCreate: LineItem[] = [] + const lineItemsToUpdate: { [id: string]: LineItem }[] = [] + for (const item of items) { + let currentItem: LineItem | undefined + + const existingItem = existingItemsVariantMap.get(item.variant_id) + if (item.should_merge) { + if (existingItem && isEqual(existingItem.metadata, item.metadata)) { + currentItem = existingItem + } + } + + // If content matches one of the line items currently in the cart we can + // simply update the quantity of the existing line item + item.quantity = currentItem + ? (currentItem.quantity += item.quantity) + : item.quantity + + await inventoryServiceTx.confirmInventory( + item.variant_id, + item.quantity + ) + + if (currentItem) { + lineItemsToUpdate[currentItem.id] = { + quantity: item.quantity, + has_shipping: false, + } + } else { + // Since the variant is eager loaded, we are removing it before the line item is being created. + delete (item as Partial).variant + item.has_shipping = false + item.cart_id = cart.id + lineItemsToCreate.push(item) + } + } + + const itemKeysToUpdate = Object.keys(lineItemsToUpdate) + + // Update all items that needs to be updated + if (itemKeysToUpdate.length) { + await Promise.all( + itemKeysToUpdate.map(async (id) => { + return await lineItemServiceTx.update(id, lineItemsToUpdate[id]) + }) + ) + } + + // Create all items that needs to be created + await lineItemServiceTx.create(lineItemsToCreate) + + await lineItemServiceTx + .update( + { + cart_id: cartId, + has_shipping: true, + }, + { has_shipping: false } + ) + .catch((err: Error | MedusaError) => { + // We only want to catch the errors related to not found items since we don't care if there is not item to update + if ("type" in err && err.type === MedusaError.Types.NOT_FOUND) { + return + } + throw err + }) cart = await this.retrieve(cart.id, { relations: ["items", "discounts", "discounts.rule", "region"], diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index a5b39818e6..0253c4f70a 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -41,6 +41,7 @@ import { isFuture, isPast } from "../utils/date-helpers" import { FlagRouter } from "../utils/flag-router" import CustomerService from "./customer" import DiscountConditionService from "./discount-condition" +import { CalculationContextData } from "../types/totals" /** * Provides layer to manipulate discounts. @@ -573,7 +574,7 @@ class DiscountService extends TransactionBaseService { async calculateDiscountForLineItem( discountId: string, lineItem: LineItem, - cart: Cart + calculationContextData: CalculationContextData ): Promise { return await this.atomicPhase_(async (transactionManager) => { let adjustment = 0 @@ -586,6 +587,12 @@ class DiscountService extends TransactionBaseService { const { type, value, allocation } = discount.rule + const calculationContext = await this.totalsService_ + .withTransaction(transactionManager) + .getCalculationContext(calculationContextData, { + exclude_shipping: true, + }) + let fullItemPrice = lineItem.unit_price * lineItem.quantity if ( this.featureFlagRouter_.isFeatureEnabled( @@ -593,11 +600,6 @@ class DiscountService extends TransactionBaseService { ) && lineItem.includes_tax ) { - const calculationContext = await this.totalsService_ - .withTransaction(transactionManager) - .getCalculationContext(cart, { - exclude_shipping: true, - }) const lineItemTotals = await this.newTotalsService_ .withTransaction(transactionManager) .getLineItemTotals([lineItem], { @@ -616,15 +618,26 @@ class DiscountService extends TransactionBaseService { // when a fixed discount should be applied to the total, // we create line adjustments for each item with an amount // relative to the subtotal - const subtotal = await this.totalsService_.getSubtotal(cart, { - excludeNonDiscounts: true, - }) + const discountedItems = calculationContextData.items.filter( + (item) => item.allow_discounts + ) + const totals = await this.newTotalsService_.getLineItemTotals( + discountedItems, + { + calculationContext, + } + ) + const subtotal = Object.values(totals).reduce((subtotal, total) => { + subtotal += total.subtotal + return subtotal + }, 0) const nominator = Math.min(value, subtotal) const totalItemPercentage = fullItemPrice / subtotal adjustment = Math.round(nominator * totalItemPercentage) } else { adjustment = value * lineItem.quantity } + // if the amount of the discount exceeds the total price of the item, // we return the total item price, else the fixed amount return adjustment >= fullItemPrice ? fullItemPrice : adjustment diff --git a/packages/medusa/src/services/event-bus.ts b/packages/medusa/src/services/event-bus.ts index 1f4f5fc234..d18cb16d92 100644 --- a/packages/medusa/src/services/event-bus.ts +++ b/packages/medusa/src/services/event-bus.ts @@ -55,28 +55,28 @@ export default class EventBusService { config: ConfigModule, singleton = true ) { - const opts = { - createClient: (type: string): Redis.Redis => { - switch (type) { - case "client": - return redisClient - case "subscriber": - return redisSubscriber - default: - if (config.projectConfig.redis_url) { - return new Redis(config.projectConfig.redis_url) - } - return redisClient - } - }, - } - this.config_ = config this.manager_ = manager this.logger_ = logger this.stagedJobRepository_ = stagedJobRepository if (singleton) { + const opts = { + createClient: (type: string): Redis.Redis => { + switch (type) { + case "client": + return redisClient + case "subscriber": + return redisSubscriber + default: + if (config.projectConfig.redis_url) { + return new Redis(config.projectConfig.redis_url) + } + return redisClient + } + }, + } + this.observers_ = new Map() this.queue_ = new Bull(`${this.constructor.name}:queue`, opts) this.cronHandlers_ = new Map() diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index 8f936b60ba..b4aa7673e4 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -2,6 +2,7 @@ export { default as AnalyticsConfigService } from "./analytics-config" export { default as AuthService } from "./auth" export { default as BatchJobService } from "./batch-job" export { default as CartService } from "./cart" +export { default as CacheService } from "./cache" export { default as ClaimService } from "./claim" export { default as ClaimItemService } from "./claim-item" export { default as CurrencyService } from "./currency" @@ -35,7 +36,7 @@ export { default as ProductService } from "./product" export { default as ProductCollectionService } from "./product-collection" export { default as ProductTypeService } from "./product-type" export { default as ProductVariantService } from "./product-variant" -import { default as PublishableApiKey } from "./publishable-api-key" + export { default as RegionService } from "./region" export { default as ReturnService } from "./return" export { default as ReturnReasonService } from "./return-reason" diff --git a/packages/medusa/src/services/inventory.ts b/packages/medusa/src/services/inventory.ts index 67dea75637..73559f751c 100644 --- a/packages/medusa/src/services/inventory.ts +++ b/packages/medusa/src/services/inventory.ts @@ -3,6 +3,7 @@ import { TransactionBaseService } from "../interfaces" import { EntityManager } from "typeorm" import ProductVariantService from "./product-variant" import { ProductVariant } from "../models" +import { isDefined } from "../utils" type InventoryServiceProps = { manager: EntityManager @@ -62,22 +63,29 @@ class InventoryService extends TransactionBaseService { * @return true if the inventory covers the quantity */ async confirmInventory( - variantId: string | undefined | null, + variantId: string | null | undefined, quantity: number ): Promise { // if variantId is undefined then confirm inventory as it // is a custom item that is not managed - if (typeof variantId === "undefined" || variantId === null) { + if (!isDefined(variantId) || variantId === null) { return true } const variant = await this.productVariantService_ .withTransaction(this.manager_) - .retrieve(variantId) + .retrieve(variantId, { + select: [ + "id", + "inventory_quantity", + "allow_backorder", + "manage_inventory", + ], + }) + const { inventory_quantity, allow_backorder, manage_inventory } = variant const isCovered = !manage_inventory || allow_backorder || inventory_quantity >= quantity - if (!isCovered) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, diff --git a/packages/medusa/src/services/line-item-adjustment.ts b/packages/medusa/src/services/line-item-adjustment.ts index 7ee16d272b..62a7dc6159 100644 --- a/packages/medusa/src/services/line-item-adjustment.ts +++ b/packages/medusa/src/services/line-item-adjustment.ts @@ -1,19 +1,14 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, In } from "typeorm" -import { - Cart, - DiscountRuleType, - LineItem, - LineItemAdjustment, - ProductVariant, -} from "../models" +import { Cart, DiscountRuleType, LineItem, LineItemAdjustment } from "../models" import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment" import { FindConfig } from "../types/common" import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment" import DiscountService from "./discount" import { TransactionBaseService } from "../interfaces" import { buildQuery, setMetadata } from "../utils" +import { CalculationContextData } from "../types/totals" type LineItemAdjustmentServiceProps = { manager: EntityManager @@ -22,7 +17,7 @@ type LineItemAdjustmentServiceProps = { } type AdjustmentContext = { - variant: ProductVariant + variant: { product_id: string } } type GeneratedAdjustment = { @@ -174,13 +169,13 @@ class LineItemAdjustmentService extends TransactionBaseService { /** * Creates adjustment for a line item - * @param cart - the cart object holding discounts + * @param calculationContextData - the calculationContextData object holding discounts * @param generatedLineItem - the line item for which a line item adjustment might be created * @param context - the line item for which a line item adjustment might be created * @return a line item adjustment or undefined if no adjustment was created */ async generateAdjustments( - cart: Cart, + calculationContextData: CalculationContextData, generatedLineItem: LineItem, context: AdjustmentContext ): Promise { @@ -196,12 +191,12 @@ class LineItemAdjustmentService extends TransactionBaseService { if ( !lineItem.allow_discounts || lineItem.is_return || - !cart?.discounts?.length + !calculationContextData?.discounts?.length ) { return [] } - const [discount] = cart.discounts.filter( + const [discount] = calculationContextData.discounts.filter( (d) => d.rule.type !== DiscountRuleType.FREE_SHIPPING ) @@ -226,7 +221,7 @@ class LineItemAdjustmentService extends TransactionBaseService { const amount = await this.discountService.calculateDiscountForLineItem( discount.id, lineItem, - cart + calculationContextData ) // if discounted amount is 0, then do nothing @@ -234,15 +229,13 @@ class LineItemAdjustmentService extends TransactionBaseService { return [] } - const adjustments = [ + return [ { amount, discount_id: discount.id, description: "discount", }, ] - - return adjustments }) } diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index e2bea63406..b1b5e7f504 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -5,7 +5,12 @@ import { DeepPartial } from "typeorm/common/DeepPartial" import { CartRepository } from "../repositories/cart" import { LineItemRepository } from "../repositories/line-item" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" -import { Cart, LineItem, LineItemAdjustment, LineItemTaxLine } from "../models" +import { + LineItem, + LineItemAdjustment, + LineItemTaxLine, + ProductVariant, +} from "../models" import { FindConfig, Selector } from "../types/common" import { FlagRouter } from "../utils/flag-router" import LineItemAdjustmentService from "./line-item-adjustment" @@ -18,8 +23,10 @@ import { RegionService, TaxProviderService, } from "./index" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isString, setMetadata } from "../utils" import { TransactionBaseService } from "../interfaces" +import { GenerateInputData, GenerateLineItemContext } from "../types/line-item" +import { ProductVariantPricing } from "../types/pricing" type InjectedDependencies = { manager: EntityManager @@ -178,115 +185,205 @@ class LineItemService extends TransactionBaseService { ) } - async generate( - variantId: string, - regionId: string, - quantity: number, - context: { - unit_price?: number - includes_tax?: boolean - metadata?: Record - customer_id?: string - order_edit_id?: string - cart?: Cart - } = {} - ): Promise { + /** + * Generate a single or multiple line item without persisting the data into the db + * @param variantIdOrData + * @param regionIdOrContext + * @param quantity + * @param context + */ + async generate< + T = string | GenerateInputData | GenerateInputData[], + TResult = T extends string + ? LineItem + : T extends LineItem + ? LineItem + : LineItem[] + >( + variantIdOrData: string | T, + regionIdOrContext: T extends string ? string : GenerateLineItemContext, + quantity?: number, + context: GenerateLineItemContext = {} + ): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { - const [variant, region] = await Promise.all([ - this.productVariantService_ - .withTransaction(transactionManager) - .retrieve(variantId, { - relations: ["product"], - }), - this.regionService_ - .withTransaction(transactionManager) - .retrieve(regionId), - ]) - - let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price - - let unitPriceIncludesTax = false - - let shouldMerge = false - - if (context.unit_price === undefined || context.unit_price === null) { - shouldMerge = true - const variantPricing = await this.pricingService_ - .withTransaction(transactionManager) - .getProductVariantPricingById(variant.id, { - region_id: region.id, - quantity: quantity, - customer_id: context?.customer_id, - include_discount_prices: true, - }) - - unitPriceIncludesTax = !!variantPricing.calculated_price_includes_tax - - unit_price = variantPricing.calculated_price ?? undefined - } - - const rawLineItem: Partial = { - unit_price: unit_price, - title: variant.product.title, - description: variant.title, - thumbnail: variant.product.thumbnail, - variant_id: variant.id, - quantity: quantity || 1, - allow_discounts: variant.product.discountable, - is_giftcard: variant.product.is_giftcard, - metadata: context?.metadata || {}, - should_merge: shouldMerge, - } - - if ( - this.featureFlagRouter_.isFeatureEnabled( - TaxInclusivePricingFeatureFlag.key - ) - ) { - rawLineItem.includes_tax = unitPriceIncludesTax - } - - if ( - this.featureFlagRouter_.isFeatureEnabled(OrderEditingFeatureFlag.key) - ) { - rawLineItem.order_edit_id = context.order_edit_id || null - } - - const lineItemRepo = transactionManager.getCustomRepository( - this.lineItemRepository_ + this.validateGenerateArguments( + variantIdOrData, + regionIdOrContext, + quantity ) - const lineItem = lineItemRepo.create({ - ...rawLineItem, - variant, - }) - if (context.cart) { - const adjustments = await this.lineItemAdjustmentService_ - .withTransaction(transactionManager) - .generateAdjustments(context.cart, lineItem, { variant }) - lineItem.adjustments = adjustments as unknown as LineItemAdjustment[] + const data = isString(variantIdOrData) + ? { + variantId: variantIdOrData, + quantity: quantity as number, + } + : variantIdOrData + const resolvedContext = isString(variantIdOrData) + ? context + : (regionIdOrContext as GenerateLineItemContext) + const regionId = ( + isString(variantIdOrData) + ? regionIdOrContext + : resolvedContext.region_id + ) as string + + const resolvedData = ( + Array.isArray(data) ? data : [data] + ) as GenerateInputData[] + + const variants = await this.productVariantService_.list( + { + id: resolvedData.map((d) => d.variantId), + }, + { + relations: ["product"], + } + ) + + const variantsMap = new Map() + const variantIdsToCalculatePricingFor: string[] = [] + + for (const variant of variants) { + variantsMap.set(variant.id, variant) + if (resolvedContext.unit_price == null) { + variantIdsToCalculatePricingFor.push(variant.id) + } } - return lineItem + const variantsPricing = await this.pricingService_ + .withTransaction(transactionManager) + .getProductVariantsPricing(variantIdsToCalculatePricingFor, { + region_id: regionId, + quantity: quantity, + customer_id: context?.customer_id, + include_discount_prices: true, + }) + + const generatedItems: LineItem[] = [] + + for (const variantData of resolvedData) { + const variant = variantsMap.get( + variantData.variantId + ) as ProductVariant + const variantPricing = variantsPricing[variantData.variantId] + + const lineItem = await this.generateLineItem( + variant, + variantData.quantity, + { + ...resolvedContext, + variantPricing, + } + ) + + if (resolvedContext.cart) { + const adjustments = await this.lineItemAdjustmentService_ + .withTransaction(transactionManager) + .generateAdjustments(resolvedContext.cart, lineItem, { variant }) + lineItem.adjustments = + adjustments as unknown as LineItemAdjustment[] + } + + generatedItems.push(lineItem) + } + + return (Array.isArray(data) + ? generatedItems + : generatedItems[0]) as unknown as TResult } ) } + protected async generateLineItem( + variant: { + id: string + title: string + product_id: string + product: { + title: string + thumbnail: string | null + discountable: boolean + is_giftcard: boolean + } + }, + quantity: number, + context: GenerateLineItemContext & { + variantPricing: ProductVariantPricing + } + ): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + + let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price + let unitPriceIncludesTax = false + let shouldMerge = false + + if (context.unit_price == null) { + shouldMerge = true + + unitPriceIncludesTax = + !!context.variantPricing?.calculated_price_includes_tax + unit_price = context.variantPricing?.calculated_price ?? undefined + } + + const rawLineItem: Partial = { + unit_price: unit_price, + title: variant.product.title, + description: variant.title, + thumbnail: variant.product.thumbnail, + variant_id: variant.id, + quantity: quantity || 1, + allow_discounts: variant.product.discountable, + is_giftcard: variant.product.is_giftcard, + metadata: context?.metadata || {}, + should_merge: shouldMerge, + } + + if ( + this.featureFlagRouter_.isFeatureEnabled( + TaxInclusivePricingFeatureFlag.key + ) + ) { + rawLineItem.includes_tax = unitPriceIncludesTax + } + + if (this.featureFlagRouter_.isFeatureEnabled(OrderEditingFeatureFlag.key)) { + rawLineItem.order_edit_id = context.order_edit_id || null + } + + const lineItemRepo = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + const lineItem = lineItemRepo.create(rawLineItem) + lineItem.variant = variant as ProductVariant + + return lineItem + } + /** * Create a line item * @param data - the line item object to create * @return the created line item */ - async create(data: Partial): Promise { + async create< + T = LineItem | LineItem[], + TResult = T extends LineItem ? LineItem : LineItem[] + >(data: T): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const lineItemRepository = transactionManager.getCustomRepository( this.lineItemRepository_ ) - const item = lineItemRepository.create(data) - return await lineItemRepository.save(item) + const data_ = Array.isArray(data) ? data : [data] + + const items = lineItemRepository.create(data_) + const lineItems = await lineItemRepository.save(items) + + return (Array.isArray(data) + ? lineItems + : lineItems[0]) as unknown as TResult } ) } @@ -326,14 +423,8 @@ class LineItemService extends TransactionBaseService { } lineItems = lineItems.map((item) => { - const lineItemMetadata = metadata - ? setMetadata(item, metadata) - : item.metadata - - return Object.assign(item, { - ...rest, - metadata: lineItemMetadata, - }) + item.metadata = metadata ? setMetadata(item, metadata) : item.metadata + return Object.assign(item, rest) }) return await lineItemRepository.save(lineItems) @@ -464,6 +555,37 @@ class LineItemService extends TransactionBaseService { return await lineItemRepository.save(clonedLineItemEntities) }) } + + protected validateGenerateArguments< + T = string | GenerateInputData | GenerateInputData[], + TResult = T extends string + ? LineItem + : T extends LineItem + ? LineItem + : LineItem[] + >( + variantIdOrData: string | T, + regionIdOrContext: T extends string ? string : GenerateLineItemContext, + quantity?: number + ): void | never { + if (isString(variantIdOrData)) { + if (!quantity || !regionIdOrContext || !isString(regionIdOrContext)) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "The generate method has been called with a variant id but one of the argument quantity or regionId is missing. Please, provide the variantId, quantity and regionId." + ) + } + } else { + const resolvedContext = regionIdOrContext as GenerateLineItemContext + + if (!resolvedContext.region_id) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "The generate method has been called with the data but the context is missing either region_id or region. Please provide at least one of region or region_id." + ) + } + } + } } export default LineItemService diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index fb8cdddbb2..93b155b776 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -7,7 +7,7 @@ import { PriceSelectionContext, } from "../interfaces/price-selection-strategy" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" -import { Product, ProductVariant, ShippingOption } from "../models" +import { Product, ProductVariant, Region, ShippingOption } from "../models" import { PricedProduct, PricedShippingOption, @@ -73,8 +73,9 @@ class PricingService extends TransactionBaseService { let taxRate: number | null = null let currencyCode = context.currency_code + let region: Region if (context.region_id) { - const region = await this.regionService + region = await this.regionService .withTransaction(this.manager_) .retrieve(context.region_id, { select: ["id", "currency_code", "automatic_taxes", "tax_rate"], @@ -247,6 +248,7 @@ class PricingService extends TransactionBaseService { * @param variantId - the id of the variant to get prices for * @param context - the price selection context to use * @return The product variant prices + * @deprecated Use {@link getProductVariantsPricing} instead. */ async getProductVariantPricingById( variantId: string, @@ -267,6 +269,7 @@ class PricingService extends TransactionBaseService { const { product_id } = await this.productVariantService .withTransaction(this.manager_) .retrieve(variantId, { select: ["id", "product_id"] }) + productRates = await this.taxProviderService .withTransaction(this.manager_) .getRegionRatesForProduct(product_id, { @@ -282,6 +285,69 @@ class PricingService extends TransactionBaseService { ) } + /** + * Gets the prices for a collection of variants. + * @param variantIds - the id of the variants to get the prices for + * @param context - the price selection context to use + * @return The product variant prices + */ + async getProductVariantsPricing< + T = string | string[], + TOutput = T extends string + ? ProductVariantPricing + : { [variant_id: string]: ProductVariantPricing } + >( + variantIds: T, + context: PriceSelectionContext | PricingContext + ): Promise { + let pricingContext: PricingContext + if ("automatic_taxes" in context) { + pricingContext = context + } else { + pricingContext = await this.collectPricingContext(context) + } + + const ids = ( + Array.isArray(variantIds) ? variantIds : [variantIds] + ) as string[] + + const variants = await this.productVariantService + .withTransaction(this.manager_) + .list({ id: ids }, { select: ["id", "product_id"] }) + + const variantsMap = new Map( + variants.map((variant) => { + return [variant.id, variant] + }) + ) + + const pricingResult: { [variant_id: string]: ProductVariantPricing } = {} + for (const variantId of ids) { + const { id, product_id } = variantsMap.get(variantId)! + + let productRates: TaxServiceRate[] = [] + + if (pricingContext.price_selection.region_id) { + productRates = await this.taxProviderService + .withTransaction(this.manager_) + .getRegionRatesForProduct(product_id, { + id: pricingContext.price_selection.region_id, + tax_rate: pricingContext.tax_rate, + }) + } + + pricingResult[id] = await this.getProductVariantPricing_( + id, + productRates, + pricingContext + ) + } + + return (!Array.isArray(variantIds) + ? Object.values(pricingResult)[0] + : pricingResult) as unknown as TOutput + } + private async getProductPricing_( productId: string, variants: ProductVariant[], diff --git a/packages/medusa/src/services/totals.ts b/packages/medusa/src/services/totals.ts index 936d6d085c..524225ca64 100644 --- a/packages/medusa/src/services/totals.ts +++ b/packages/medusa/src/services/totals.ts @@ -6,6 +6,7 @@ import { } from "../interfaces" import { Cart, + ClaimOrder, Discount, DiscountRuleType, LineItem, @@ -13,10 +14,12 @@ import { Order, ShippingMethod, ShippingMethodTaxLine, + Swap, } from "../models" import { isCart } from "../types/cart" import { isOrder } from "../types/orders" import { + CalculationContextData, LineAllocationsMap, LineDiscount, LineDiscountAmount, @@ -429,7 +432,12 @@ class TotalsService extends TransactionBaseService { * @return the allocation map for the line items in the cart or order. */ async getAllocationMap( - orderOrCart: Cart | Order, + orderOrCart: { + discounts?: Discount[] + items: LineItem[] + swaps?: Swap[] + claims?: ClaimOrder[] + }, options: AllocationMapOptions = {} ): Promise { const allocationMap: LineAllocationsMap = {} @@ -700,19 +708,23 @@ class TotalsService extends TransactionBaseService { * order */ getLineDiscounts( - cartOrOrder: Cart | Order, + cartOrOrder: { + items: LineItem[] + swaps?: Swap[] + claims?: ClaimOrder[] + }, discount: Discount ): LineDiscountAmount[] { let merged: LineItem[] = [...(cartOrOrder.items ?? [])] // merge items from order with items from order swaps - if ("swaps" in cartOrOrder && cartOrOrder.swaps.length) { + if ("swaps" in cartOrOrder && cartOrOrder.swaps?.length) { for (const s of cartOrOrder.swaps) { merged = [...merged, ...s.additional_items] } } - if ("claims" in cartOrOrder && cartOrOrder.claims.length) { + if ("claims" in cartOrOrder && cartOrOrder.claims?.length) { for (const c of cartOrOrder.claims) { merged = [...merged, ...c.additional_items] } @@ -1051,15 +1063,15 @@ class TotalsService extends TransactionBaseService { /** * Prepares the calculation context for a tax total calculation. - * @param cartOrOrder - the cart or order to get the calculation context for + * @param calculationContextData - the calculationContextData to get the calculation context for * @param options - options to gather context by * @return the tax calculation context */ async getCalculationContext( - cartOrOrder: Cart | Order, + calculationContextData: CalculationContextData, options: CalculationContextOptions = {} ): Promise { - const allocationMap = await this.getAllocationMap(cartOrOrder, { + const allocationMap = await this.getAllocationMap(calculationContextData, { exclude_gift_cards: options.exclude_gift_cards, exclude_discounts: options.exclude_discounts, }) @@ -1067,14 +1079,14 @@ class TotalsService extends TransactionBaseService { let shippingMethods: ShippingMethod[] = [] // Default to include shipping methods if (!options.exclude_shipping) { - shippingMethods = cartOrOrder.shipping_methods || [] + shippingMethods = calculationContextData.shipping_methods || [] } return { - shipping_address: cartOrOrder.shipping_address, + shipping_address: calculationContextData.shipping_address, shipping_methods: shippingMethods, - customer: cartOrOrder.customer, - region: cartOrOrder.region, + customer: calculationContextData.customer, + region: calculationContextData.region, is_return: options.is_return ?? false, allocation_map: allocationMap, } diff --git a/packages/medusa/src/strategies/__tests__/price-selection.js b/packages/medusa/src/strategies/__tests__/price-selection.js index be02940965..dcedc69f32 100644 --- a/packages/medusa/src/strategies/__tests__/price-selection.js +++ b/packages/medusa/src/strategies/__tests__/price-selection.js @@ -1,6 +1,7 @@ import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing" import { FlagRouter } from "../../utils/flag-router" import PriceSelectionStrategy from "../price-selection" +import { cacheServiceMock } from "../../services/__mocks__/cache" const executeTest = (flagValue) => @@ -226,6 +227,7 @@ const executeTest = manager: mockEntityManager, moneyAmountRepository: mockMoneyAmountRepository, featureFlagRouter, + cacheService: cacheServiceMock, }) try { diff --git a/packages/medusa/src/strategies/batch-jobs/product/export.ts b/packages/medusa/src/strategies/batch-jobs/product/export.ts index c2a03933bd..37dfb31519 100644 --- a/packages/medusa/src/strategies/batch-jobs/product/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/product/export.ts @@ -211,13 +211,19 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy { return await this.atomicPhase_( async (transactionManager) => { - let batchJob = (await this.batchJobService_ - .withTransaction(transactionManager) - .retrieve(batchJobId)) as ProductExportBatchJob + const productServiceTx = + this.productService_.withTransaction(transactionManager) + const batchJobServiceTx = + this.batchJobService_.withTransaction(transactionManager) + const fileServiceTx = + this.fileService_.withTransaction(transactionManager) - const { writeStream, fileKey, promise } = await this.fileService_ - .withTransaction(transactionManager) - .getUploadStreamDescriptor({ + let batchJob = (await batchJobServiceTx.retrieve( + batchJobId + )) as ProductExportBatchJob + + const { writeStream, fileKey, promise } = + await fileServiceTx.getUploadStreamDescriptor({ name: `exports/products/product-export-${Date.now()}`, ext: "csv", }) @@ -226,14 +232,12 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy { writeStream.write(header) approximateFileSize += Buffer.from(header).byteLength - await this.batchJobService_ - .withTransaction(transactionManager) - .update(batchJobId, { - result: { - file_key: fileKey, - file_size: approximateFileSize, - }, - }) + await batchJobServiceTx.update(batchJobId, { + result: { + file_key: fileKey, + file_size: approximateFileSize, + }, + }) advancementCount = batchJob.result?.advancement_count ?? advancementCount @@ -241,26 +245,25 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy { limit = batchJob.context?.list_config?.take ?? limit const { list_config = {}, filterable_fields = {} } = batchJob.context - const [productList, count] = await this.productService_ - .withTransaction(transactionManager) - .listAndCount(filterable_fields, { + const [productList, count] = await productServiceTx.listAndCount( + filterable_fields, + { ...list_config, skip: offset, take: Math.min(batchJob.context.batch_size ?? Infinity, limit), - } as FindProductConfig) + } as FindProductConfig + ) productCount = batchJob.context?.batch_size ?? count let products: Product[] = productList while (offset < productCount) { if (!products?.length) { - products = await this.productService_ - .withTransaction(transactionManager) - .list(filterable_fields, { - ...list_config, - skip: offset, - take: Math.min(productCount - offset, limit), - } as FindProductConfig) + products = await productServiceTx.list(filterable_fields, { + ...list_config, + skip: offset, + take: Math.min(productCount - offset, limit), + } as FindProductConfig) } products.forEach((product: Product) => { @@ -275,16 +278,14 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy { offset += products.length products = [] - batchJob = (await this.batchJobService_ - .withTransaction(transactionManager) - .update(batchJobId, { - result: { - file_size: approximateFileSize, - count: productCount, - advancement_count: advancementCount, - progress: advancementCount / productCount, - }, - })) as ProductExportBatchJob + batchJob = (await batchJobServiceTx.update(batchJobId, { + result: { + file_size: approximateFileSize, + count: productCount, + advancement_count: advancementCount, + progress: advancementCount / productCount, + }, + })) as ProductExportBatchJob if (batchJob.status === BatchJobStatus.CANCELED) { writeStream.end() diff --git a/packages/medusa/src/strategies/price-selection.ts b/packages/medusa/src/strategies/price-selection.ts index 65b4ea54cb..94a4fc6e20 100644 --- a/packages/medusa/src/strategies/price-selection.ts +++ b/packages/medusa/src/strategies/price-selection.ts @@ -1,27 +1,36 @@ import { EntityManager } from "typeorm" import { AbstractPriceSelectionStrategy, + ICacheService, IPriceSelectionStrategy, PriceSelectionContext, PriceSelectionResult, PriceType, -} from "../interfaces/price-selection-strategy" +} from "../interfaces" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { MoneyAmountRepository } from "../repositories/money-amount" import { TaxServiceRate } from "../types/tax-service" import { FlagRouter } from "../utils/flag-router" -import { isDefined } from "../utils/is-defined" +import { isDefined } from "../utils" class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { - private moneyAmountRepository_: typeof MoneyAmountRepository - private featureFlagRouter_: FlagRouter - private manager_: EntityManager + protected manager_: EntityManager - constructor({ manager, featureFlagRouter, moneyAmountRepository }) { + protected readonly featureFlagRouter_: FlagRouter + protected moneyAmountRepository_: typeof MoneyAmountRepository + protected cacheService_: ICacheService + + constructor({ + manager, + featureFlagRouter, + moneyAmountRepository, + cacheService, + }) { super() this.manager_ = manager this.moneyAmountRepository_ = moneyAmountRepository this.featureFlagRouter_ = featureFlagRouter + this.cacheService_ = cacheService } withTransaction(manager: EntityManager): IPriceSelectionStrategy { @@ -33,6 +42,7 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { manager: manager, moneyAmountRepository: this.moneyAmountRepository_, featureFlagRouter: this.featureFlagRouter_, + cacheService: this.cacheService_, }) } @@ -40,14 +50,30 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { variant_id: string, context: PriceSelectionContext ): Promise { + // TODO: Refactor using the cache decorators when it will be finished + const cacheKey = this.getCacheKey(variant_id, context) + const cached = await this.cacheService_ + .get(cacheKey) + .catch(() => void 0) + if (cached) { + return cached + } + + let result + if ( this.featureFlagRouter_.isFeatureEnabled( TaxInclusivePricingFeatureFlag.key ) ) { - return this.calculateVariantPrice_new(variant_id, context) + result = await this.calculateVariantPrice_new(variant_id, context) + } else { + result = await this.calculateVariantPrice_old(variant_id, context) } - return this.calculateVariantPrice_old(variant_id, context) + + await this.cacheService_.set(cacheKey, result) + + return result } private async calculateVariantPrice_new( @@ -213,6 +239,21 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { return result } + + private getCacheKey( + variantId: string, + context: PriceSelectionContext + ): string { + const taxRate = + context.tax_rates?.reduce( + (accRate: number, nextTaxRate: TaxServiceRate) => { + return accRate + (nextTaxRate.rate || 0) / 100 + }, + 0 + ) || 0 + + return `ps:${variantId}:${context.region_id}:${context.currency_code}:${context.customer_id}:${context.quantity}:${context.include_discount_prices}:${taxRate}` + } } const isValidAmount = ( diff --git a/packages/medusa/src/subscribers/pricing.ts b/packages/medusa/src/subscribers/pricing.ts new file mode 100644 index 0000000000..94dc4aebc0 --- /dev/null +++ b/packages/medusa/src/subscribers/pricing.ts @@ -0,0 +1,33 @@ +import { + CacheService, + EventBusService, + ProductVariantService, +} from "../services" + +type ProductVariantUpdatedEventData = { + id: string + product_id: string + fields: string[] +} + +class PricingSubscriber { + protected readonly eventBus_: EventBusService + protected readonly cacheService_: CacheService + + constructor({ eventBusService, cacheService }) { + this.eventBus_ = eventBusService + this.cacheService_ = cacheService + + this.eventBus_.subscribe( + ProductVariantService.Events.UPDATED, + async (data) => { + const { id, fields } = data as ProductVariantUpdatedEventData + if (fields.includes("prices")) { + await this.cacheService_.invalidate(`ps:${id}*`) + } + } + ) + } +} + +export default PricingSubscriber diff --git a/packages/medusa/src/types/cart.ts b/packages/medusa/src/types/cart.ts index 47f9875545..4b8a5031b2 100644 --- a/packages/medusa/src/types/cart.ts +++ b/packages/medusa/src/types/cart.ts @@ -1,11 +1,8 @@ import { ValidateNested } from "class-validator" import { IsType } from "../utils/validators/is-type" import { Cart, CartType } from "../models/cart" -import { - AddressPayload, - DateComparisonOperator, - StringComparisonOperator, -} from "./common" +import { AddressPayload, DateComparisonOperator, StringComparisonOperator } from "./common" +import { Region } from "../models" // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isCart(object: any): object is Cart { @@ -34,6 +31,11 @@ export type LineItemUpdate = { variant_id?: string } +export type LineItemValidateData = { + variant?: { product_id: string }; + variant_id: string +} + class GiftCard { code: string } @@ -44,6 +46,7 @@ class Discount { export type CartCreateProps = { region_id?: string + region?: Region email?: string billing_address_id?: string billing_address?: Partial diff --git a/packages/medusa/src/types/line-item.ts b/packages/medusa/src/types/line-item.ts new file mode 100644 index 0000000000..c1aa714896 --- /dev/null +++ b/packages/medusa/src/types/line-item.ts @@ -0,0 +1,16 @@ +import { CalculationContextData } from "./totals" + +export type GenerateInputData = { + variantId: string + quantity: number +} + +export type GenerateLineItemContext = { + region_id?: string + unit_price?: number + includes_tax?: boolean + metadata?: Record + customer_id?: string + order_edit_id?: string + cart?: CalculationContextData +} diff --git a/packages/medusa/src/types/totals.ts b/packages/medusa/src/types/totals.ts index f818e06120..f9eec6014e 100644 --- a/packages/medusa/src/types/totals.ts +++ b/packages/medusa/src/types/totals.ts @@ -1,4 +1,24 @@ -import { LineItem } from "../models" +import { + Address, + ClaimOrder, + Customer, + Discount, + LineItem, + Region, + ShippingMethod, + Swap, +} from "../models" + +export type CalculationContextData = { + discounts: Discount[] + items: LineItem[] + customer: Customer + region: Region + shipping_address: Address | null + swaps?: Swap[] + claims?: ClaimOrder[] + shipping_methods?: ShippingMethod[] +} /** The amount of a gift card allocated to a line item */ export type GiftCardAllocation = {