diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index d712a71ee3..a4a7db30fd 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -21,7 +21,7 @@ Array [ "handle": "test-product", "height": null, "hs_code": null, - "id": StringMatching /\\^test-\\*/, + "id": "test-product", "images": Array [ Object { "created_at": Any, @@ -78,6 +78,7 @@ Array [ Object { "allow_backorder": false, "barcode": "test-barcode", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -103,15 +104,17 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, "created_at": Any, "currency_code": "usd", "deleted_at": null, - "id": StringMatching /\\^test-price\\*/, + "id": "test-price", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -129,6 +132,7 @@ Array [ Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean2", @@ -154,6 +158,7 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -163,10 +168,11 @@ Array [ "id": StringMatching /\\^test-price\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, + "variant_id": "test-variant_2", }, ], "product_id": StringMatching /\\^test-\\*/, @@ -180,6 +186,7 @@ Array [ Object { "allow_backorder": false, "barcode": "test-barcode 1", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean1", @@ -205,6 +212,7 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -214,6 +222,7 @@ Array [ "id": StringMatching /\\^test-price\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -228,6 +237,60 @@ Array [ "weight": null, "width": null, }, + Object { + "allow_backorder": false, + "barcode": "test-barcode-sale", + "calculated_price": null, + "created_at": Any, + "deleted_at": null, + "ean": "test-ean-sale", + "height": null, + "hs_code": null, + "id": "test-variant-sale", + "inventory_quantity": 10, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "options": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "id": StringMatching /\\^test-variant-option\\*/, + "metadata": null, + "option_id": StringMatching /\\^test-opt\\*/, + "updated_at": Any, + "value": "Default variant", + "variant_id": StringMatching /\\^test-variant\\*/, + }, + ], + "origin_country": null, + "original_price": null, + "prices": Array [ + Object { + "amount": 1000, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price-sale", + "max_quantity": null, + "min_quantity": null, + "price_list": null, + "price_list_id": null, + "region_id": null, + "updated_at": Any, + "variant_id": StringMatching /\\^test-variant\\*/, + }, + ], + "product_id": StringMatching /\\^test-\\*/, + "sku": "test-sku-sale", + "title": "Test variant", + "upc": "test-upc-sale", + "updated_at": Any, + "weight": null, + "width": null, + }, ], "weight": null, "width": null, @@ -251,7 +314,7 @@ Array [ "handle": "test-product1", "height": null, "hs_code": null, - "id": StringMatching /\\^test-\\*/, + "id": "test-product1", "images": Array [], "is_giftcard": false, "length": null, @@ -289,6 +352,7 @@ Array [ Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean4", @@ -314,6 +378,7 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -323,6 +388,7 @@ Array [ "id": StringMatching /\\^test-price\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -340,6 +406,7 @@ Array [ Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean3", @@ -365,6 +432,7 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -374,6 +442,7 @@ Array [ "id": StringMatching /\\^test-price\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": "test-region", "updated_at": Any, @@ -392,39 +461,6 @@ Array [ "weight": null, "width": null, }, - Object { - "collection": Any, - "collection_id": "test-collection1", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product_filtering_3", - "height": null, - "hs_code": null, - "id": StringMatching /\\^test-\\*/, - "images": Array [], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Any, - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "draft", - "subtitle": null, - "tags": Any, - "thumbnail": null, - "title": "Test product filtering 3", - "type": Any, - "type_id": "test-type", - "updated_at": Any, - "variants": Any, - "weight": null, - "width": null, - }, Object { "collection": Any, "collection_id": "test-collection1", @@ -436,7 +472,7 @@ Array [ "handle": "test-product_filtering_1", "height": null, "hs_code": null, - "id": StringMatching /\\^test-\\*/, + "id": "test-product_filtering_1", "images": Array [], "is_giftcard": false, "length": null, @@ -469,7 +505,7 @@ Array [ "handle": "test-product_filtering_2", "height": null, "hs_code": null, - "id": StringMatching /\\^test-\\*/, + "id": "test-product_filtering_2", "images": Array [], "is_giftcard": false, "length": null, @@ -491,6 +527,39 @@ Array [ "weight": null, "width": null, }, + Object { + "collection": Any, + "collection_id": "test-collection1", + "created_at": Any, + "deleted_at": null, + "description": "test-product-description", + "discountable": true, + "external_id": null, + "handle": "test-product_filtering_3", + "height": null, + "hs_code": null, + "id": "test-product_filtering_3", + "images": Array [], + "is_giftcard": false, + "length": null, + "material": null, + "metadata": null, + "mid_code": null, + "options": Any, + "origin_country": null, + "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", + "subtitle": null, + "tags": Any, + "thumbnail": null, + "title": "Test product filtering 3", + "type": Any, + "type_id": "test-type", + "updated_at": Any, + "variants": Any, + "weight": null, + "width": null, + }, ] `; @@ -539,6 +608,7 @@ Array [ Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -564,6 +634,7 @@ Array [ }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -573,6 +644,7 @@ Array [ "id": Any, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -696,6 +768,7 @@ Object { Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -731,6 +804,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -740,6 +814,7 @@ Object { "id": StringMatching /\\^ma_\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -753,6 +828,7 @@ Object { "id": StringMatching /\\^ma_\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -766,6 +842,7 @@ Object { "id": StringMatching /\\^ma_\\*/, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -855,6 +932,7 @@ Object { Object { "allow_backorder": false, "barcode": "test-barcode", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -880,6 +958,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 75, @@ -889,6 +968,7 @@ Object { "id": "test-price", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index fd53816501..5a17a5a933 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -522,10 +522,13 @@ describe("/admin/products", () => { console.log(err) }) + response.data.products.sort((a, b) => + a.created_at > b.created_at ? 1 : -1 + ) + expect(response.data.products).toMatchSnapshot([ { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), + id: "test-product", options: [ { id: expect.stringMatching(/^test-*/), @@ -543,7 +546,53 @@ describe("/admin/products", () => { ], variants: [ { - id: "test-variant", // expect.stringMatching(/^test-variant*/), + id: "test-variant", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: [ + { + id: "test-price", + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + options: [ + { + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }, + { + id: "test-variant_2", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: [ + { + id: expect.stringMatching(/^test-price*/), + variant_id: "test-variant_2", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + options: [ + { + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }, + { + id: "test-variant_1", created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), @@ -566,36 +615,13 @@ describe("/admin/products", () => { ], }, { - id: "test-variant_2", // expect.stringMatching(/^test-variant*/), + id: "test-variant-sale", created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), prices: [ { - id: expect.stringMatching(/^test-price*/), - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - { - id: "test-variant_1", // expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: expect.stringMatching(/^test-price*/), + id: "test-price-sale", variant_id: expect.stringMatching(/^test-variant*/), created_at: expect.any(String), updated_at: expect.any(String), @@ -634,12 +660,12 @@ describe("/admin/products", () => { updated_at: expect.any(String), }, { - id: expect.stringMatching(/^test-*/), + id: "test-product1", created_at: expect.any(String), options: [], variants: [ { - id: "test-variant_4", // expect.stringMatching(/^test-variant*/), + id: "test-variant_4", created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), @@ -662,7 +688,7 @@ describe("/admin/products", () => { ], }, { - id: "test-variant_3", // expect.stringMatching(/^test-variant*/), + id: "test-variant_3", created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), @@ -703,11 +729,10 @@ describe("/admin/products", () => { updated_at: expect.any(String), }, profile_id: expect.stringMatching(/^sp_*/), - created_at: expect.any(String), updated_at: expect.any(String), }, { - id: expect.stringMatching(/^test-*/), + id: "test-product_filtering_1", profile_id: expect.stringMatching(/^sp_*/), created_at: expect.any(String), type: expect.any(Object), @@ -718,7 +743,7 @@ describe("/admin/products", () => { updated_at: expect.any(String), }, { - id: expect.stringMatching(/^test-*/), + id: "test-product_filtering_2", profile_id: expect.stringMatching(/^sp_*/), created_at: expect.any(String), type: expect.any(Object), @@ -729,7 +754,7 @@ describe("/admin/products", () => { updated_at: expect.any(String), }, { - id: expect.stringMatching(/^test-*/), + id: "test-product_filtering_3", profile_id: expect.stringMatching(/^sp_*/), created_at: expect.any(String), type: expect.any(Object), @@ -1357,7 +1382,6 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data).toEqual({ product: expect.objectContaining({ id: "test-product", @@ -1369,33 +1393,6 @@ describe("/admin/products", () => { amount: 1500, currency_code: "usd", }), - expect.objectContaining({ - id: "ma_test_1", - amount: 100, - currency_code: "usd", - min_quantity: 1, - max_quantity: 100, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_2", - amount: 80, - currency_code: "usd", - min_quantity: 101, - max_quantity: 500, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_3", - amount: 50, - currency_code: "usd", - min_quantity: 501, - max_quantity: 1000, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), ]), }), ]), @@ -1451,8 +1448,9 @@ describe("/admin/products", () => { prices: [ // usd price coming from the product seeder { - currency_code: "usd", + id: "test-price", amount: 100, + currency_code: "usd", }, { currency_code: "eur", @@ -1489,33 +1487,6 @@ describe("/admin/products", () => { amount: 4500, currency_code: "eur", }), - expect.objectContaining({ - id: "ma_test_1", - amount: 100, - currency_code: "usd", - min_quantity: 1, - max_quantity: 100, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_2", - amount: 80, - currency_code: "usd", - min_quantity: 101, - max_quantity: 500, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_3", - amount: 50, - currency_code: "usd", - min_quantity: 501, - max_quantity: 1000, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), ]), }), ]), @@ -1547,42 +1518,13 @@ describe("/admin/products", () => { expect(response.status).toEqual(200) - expect(response.data.product.variants[0].prices.length).toEqual( - 4 // 3 prices from Price List + 1 default price - ) + expect(response.data.product.variants[0].prices.length).toEqual(1) expect(response.data.product.variants[0].prices).toEqual( expect.arrayContaining([ expect.objectContaining({ amount: 4500, currency_code: "usd", }), - expect.objectContaining({ - id: "ma_test_1", - amount: 100, - currency_code: "usd", - min_quantity: 1, - max_quantity: 100, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_2", - amount: 80, - currency_code: "usd", - min_quantity: 101, - max_quantity: 500, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_3", - amount: 50, - currency_code: "usd", - min_quantity: 501, - max_quantity: 1000, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), ]) ) }) @@ -1614,9 +1556,8 @@ describe("/admin/products", () => { expect(response.status).toEqual(200) - expect(response.data.product.variants[0].prices.length).toEqual( - 5 // 2 default prices + 3 prices from Price List - ) + expect(response.data.product.variants[0].prices.length).toEqual(2) + expect(response.data.product.variants[0].prices).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -1627,33 +1568,6 @@ describe("/admin/products", () => { amount: 900, currency_code: "eur", }), - expect.objectContaining({ - id: "ma_test_1", - amount: 100, - currency_code: "usd", - min_quantity: 1, - max_quantity: 100, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_2", - amount: 80, - currency_code: "usd", - min_quantity: 101, - max_quantity: 500, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), - expect.objectContaining({ - id: "ma_test_3", - amount: 50, - currency_code: "usd", - min_quantity: 501, - max_quantity: 1000, - variant_id: "test-variant", - price_list_id: "pl_no_customer_groups", - }), ]) ) }) @@ -1951,7 +1865,7 @@ describe("/admin/products", () => { it("successfully creates soft-deleted product variant", async () => { const api = useApi() - const product = await api + await api .get("/admin/products/test-product", { headers: { Authorization: "bearer test_token", diff --git a/integration-tests/api/__tests__/price-selection/index.js b/integration-tests/api/__tests__/price-selection/index.js new file mode 100644 index 0000000000..c0e2ba60d8 --- /dev/null +++ b/integration-tests/api/__tests__/price-selection/index.js @@ -0,0 +1,659 @@ +const path = require("path") + +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { useDb, initDb } = require("../../../helpers/use-db") + +const adminSeeder = require("../../helpers/admin-seeder") +const promotionsSeeder = require("../../helpers/price-selection-seeder") + +jest.setTimeout(30000) + +describe("Promotions", () => { + let medusaProcess + let dbConnection + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("Money amount", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + await promotionsSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("calculated_price contains lowest price", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product?region_id=test-region") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + const lowestPrice = variant.prices.reduce( + (prev, curr) => (curr.amount < prev ? curr.amount : prev), + Infinity + ) + + expect(variant.calculated_price).toEqual(lowestPrice) + + expect(variant).toEqual( + expect.objectContaining({ original_price: 120, calculated_price: 110 }) + ) + }) + + it("returns no money amounts belonging to customer groups without login", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product?cart_id=test-cart") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.prices.length).toEqual(2) + variant.prices.forEach((price) => { + if (price.price_list) { + expect(price.price_list.customer_groups).toEqual(undefined) + } else { + expect(price.price_list).toEqual(null) + } + }) + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1", + region_id: "test-region", + currency_code: "usd", + amount: 120, + }), + expect.objectContaining({ + id: "test-price3", + region_id: "test-region", + currency_code: "usd", + price_list_id: "pl", + amount: 110, + }), + ]) + ) + }) + + it("sets default price as original price", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product?cart_id=test-cart") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + }) + + it("gets prices for currency if no region prices exist", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product?cart_id=test-cart-2") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + expect(variant.prices.length).toEqual(2) + variant.prices.forEach((price) => { + if (price.price_list) { + expect(price.price_list.customer_groups).toEqual(undefined) + } else { + expect(price.price_list).toEqual(null) + } + }) + variant.prices.forEach((price) => { + expect(price.region_id).toEqual("test-region") + expect(price.currency_code).toEqual("usd") + }) + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1", + region_id: "test-region", + currency_code: "usd", + amount: 120, + }), + expect.objectContaining({ + id: "test-price3", + region_id: "test-region", + currency_code: "usd", + price_list_id: "pl", + amount: 110, + }), + ]) + ) + }) + + it("gets prices for cart region for multi region product", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product-multi-region?cart_id=test-cart-1") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant).toEqual( + expect.objectContaining({ original_price: 130, calculated_price: 110 }) + ) + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + + expect(variant.prices.length).toEqual(2) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1-region-2", + region_id: "test-region-2", + currency_code: "dkk", + amount: 130, + }), + expect.objectContaining({ + id: "test-price3-region-2", + region_id: "test-region-2", + currency_code: "dkk", + price_list_id: "pl", + amount: 110, + }), + ]) + ) + }) + + it("gets prices for multi region product", async () => { + const api = useApi() + const res = await api + .get( + "/store/products/test-product-multi-region?region_id=test-region-2" + ) + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant).toEqual( + expect.objectContaining({ original_price: 130, calculated_price: 110 }) + ) + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + + expect(variant.prices.length).toEqual(2) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1-region-2", + region_id: "test-region-2", + currency_code: "dkk", + amount: 130, + }), + expect.objectContaining({ + id: "test-price3-region-2", + region_id: "test-region-2", + currency_code: "dkk", + price_list_id: "pl", + amount: 110, + }), + ]) + ) + }) + + it("gets prices for multi currency product", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product-multi-region?currency_code=dkk") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant).toEqual( + expect.objectContaining({ original_price: 130, calculated_price: 110 }) + ) + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + + expect(variant.prices.length).toEqual(2) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1-region-2", + region_id: "test-region-2", + currency_code: "dkk", + amount: 130, + }), + expect.objectContaining({ + id: "test-price3-region-2", + region_id: "test-region-2", + currency_code: "dkk", + price_list_id: "pl", + amount: 110, + }), + ]) + ) + }) + + it("gets moneyamounts only with valid date interval", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product-sale?cart_id=test-cart") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + const date = new Date() + + expect(variant.prices.length).toEqual(2) + variant.prices.forEach((price) => { + if (price.starts_at) { + expect(new Date(price.starts_at).getTime()).toBeLessThan( + date.getTime() + ) + } + if (price.ends_at) { + expect(new Date(price.ends_at).getTime()).toBeGreaterThan( + date.getTime() + ) + } + }) + }) + + it("gets moneyamounts with valid date intervals and finds lowest price with overlapping intervals", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product-sale-overlap?cart_id=test-cart") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + const date = new Date() + + expect(variant).toEqual( + expect.objectContaining({ + original_price: 150, + calculated_price: 120, + }) + ) + expect(variant.prices.length).toEqual(3) + variant.prices.forEach((price) => { + if (price.starts_at) { + expect(new Date(price.starts_at).getTime()).toBeLessThan( + date.getTime() + ) + } + if (price.ends_at) { + expect(new Date(price.ends_at).getTime()).toBeGreaterThan( + date.getTime() + ) + } + }) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price-sale-overlap-1", + region_id: "test-region", + currency_code: "usd", + amount: 140, + price_list_id: "pl_current_1", + }), + expect.objectContaining({ + id: "test-price1-sale-overlap", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl_current", + }), + expect.objectContaining({ + id: "test-price2-sale-overlap-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }), + ]) + ) + }) + + it("gets all prices with varying quantity limits with no quantity", async () => { + const api = useApi() + const res = await api + .get("/store/products/test-product-quantity?cart_id=test-cart") + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.prices.length).toEqual(5) + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl", + min_quantity: 10, + max_quantity: 100, + }), + expect.objectContaining({ + id: "test-price1-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl", + min_quantity: 101, + max_quantity: 1000, + }), + expect.objectContaining({ + id: "test-price2-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl", + max_quantity: 9, + }), + expect.objectContaining({ + id: "test-price3-quantity-now", + region_id: "test-region", + currency_code: "usd", + amount: 140, + price_list_id: "pl_current", + min_quantity: 101, + max_quantity: 1000, + }), + expect.objectContaining({ + id: "test-price3-quantity-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }), + ]) + ) + + expect(variant.calculated_price).toEqual(130) + expect(variant.original_price).toEqual(150) + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + }) + + it("fetches product with groups in money amounts with login", async () => { + const api = useApi() + + // customer with customer-group 5 + const authResponse = await api.post("/store/auth", { + email: "test5@email.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const res = await api + .get("/store/products/test-product?cart_id=test-cart", { + headers: { + Cookie: authCookie, + }, + }) + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.prices.length).toEqual(3) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1", + region_id: "test-region", + currency_code: "usd", + amount: 120, + }), + expect.objectContaining({ + id: "test-price3", + region_id: "test-region", + currency_code: "usd", + price_list_id: "pl", + amount: 110, + }), + expect.objectContaining({ + id: "test-price", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list: expect.objectContaining({}), + }), + ]) + ) + }) + + it("fetches product with groups and quantities in money amounts with login", async () => { + const api = useApi() + + // customer with customer-group 5 + const authResponse = await api.post("/store/auth", { + email: "test5@email.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const res = await api + .get( + "/store/products/test-product-quantity-customer?cart_id=test-cart", + { + headers: { + Cookie: authCookie, + }, + } + ) + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + expect(variant.prices.length).toEqual(6) + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 100, + min_quantity: 10, + max_quantity: 100, + }), + expect.objectContaining({ + id: "test-price1-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + }), + expect.objectContaining({ + id: "test-price2-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 130, + max_quantity: 9, + }), + expect.objectContaining({ + id: "test-price2-quantity-customer-group", + region_id: "test-region", + currency_code: "usd", + amount: 100, + max_quantity: 9, + price_list: expect.objectContaining({}), + }), + expect.objectContaining({ + id: "test-price3-quantity-customer-now", + region_id: "test-region", + currency_code: "usd", + amount: 140, + min_quantity: 101, + max_quantity: 1000, + }), + expect.objectContaining({ + id: "test-price3-quantity-customer-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + price_list_id: null, + }), + ]) + ) + + expect(variant.calculated_price).toEqual(100) + expect(variant.original_price).toEqual(150) + expect(variant.original_price).toEqual( + variant.prices.find((p) => p.price_list_id === null).amount + ) + }) + + it("gets moneyamounts only with valid date interval for customer", async () => { + const api = useApi() + + // customer with customer-group 5 + const authResponse = await api.post("/store/auth", { + email: "test5@email.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const res = await api + .get("/store/products/test-product-sale-customer?cart_id=test-cart", { + headers: { + Cookie: authCookie, + }, + }) + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + const date = new Date() + + expect(variant).toEqual( + expect.objectContaining({ + original_price: 150, + calculated_price: 100, + }) + ) + + expect(variant.prices.length).toEqual(2) + variant.prices.forEach((price) => { + if (price.starts_at) { + expect(new Date(price.starts_at).getTime()).toBeLessThan( + date.getTime() + ) + } + if (price.ends_at) { + expect(new Date(price.ends_at).getTime()).toBeGreaterThan( + date.getTime() + ) + } + }) + }) + + it("gets moneyamounts only with valid date interval for customer regardless of quantity limits", async () => { + const api = useApi() + + // customer with customer-group 5 + const authResponse = await api.post("/store/auth", { + email: "test5@email.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const res = await api + .get( + "/store/products/test-product-sale-customer-quantity?cart_id=test-cart", + { + headers: { + Cookie: authCookie, + }, + } + ) + .catch((error) => console.log(error)) + + const variant = res.data.product.variants[0] + + const date = new Date() + + expect(variant).toEqual( + expect.objectContaining({ + original_price: 150, + calculated_price: 100, + }) + ) + + expect(variant.prices.length).toEqual(3) + variant.prices.forEach((price) => { + if (price.starts_at) { + expect(new Date(price.starts_at).getTime()).toBeLessThan( + date.getTime() + ) + } + if (price.ends_at) { + expect(new Date(price.ends_at).getTime()).toBeGreaterThan( + date.getTime() + ) + } + }) + + expect(variant.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1-sale-customer-quantity-groups", + region_id: "test-region", + currency_code: "usd", + amount: 100, + max_quantity: 99, + price_list: expect.objectContaining({}), + }), + expect.objectContaining({ + id: "test-price2-sale-customer-quantity-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }), + expect.objectContaining({ + id: "test-price1-sale-customer-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 110, + max_quantity: 99, + }), + ]) + ) + }) + }) +}) diff --git a/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap index d8417366b6..24ae2723b4 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap @@ -1,10 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`/store/variants /test-variant 1`] = ` +exports[`/store/variants GET /store/variants /test-variant 1`] = ` Object { "variant": Object { "allow_backorder": false, "barcode": "test-barcode", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -30,6 +31,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -39,6 +41,7 @@ Object { "id": "test-price", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -57,71 +60,99 @@ Object { } `; -exports[`/store/variants includes default relations 1`] = ` +exports[`/store/variants GET /store/variants advanced pricing /test-variant with currency code 1`] = ` Object { - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": "test-barcode", - "created_at": Any, - "deleted_at": null, - "ean": "test-ean", - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { + "variant": Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": 80, + "calculated_price_type": "sale", + "created_at": Any, + "deleted_at": null, + "ean": "test-ean", + "height": null, + "hs_code": null, + "id": "test-variant", + "inventory_quantity": 10, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "options": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "id": "test-variant-option", + "metadata": null, + "option_id": "test-option", + "updated_at": Any, + "value": "Default variant", + "variant_id": "test-variant", + }, + ], + "origin_country": null, + "original_price": 100, + "prices": Array [ + Object { + "amount": 100, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price", + "max_quantity": null, + "min_quantity": null, + "price_list": null, + "price_list_id": null, + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, + Object { + "amount": 80, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price-discount", + "max_quantity": null, + "min_quantity": null, + "price_list": Object { "created_at": Any, "deleted_at": null, - "id": "test-variant-option", - "metadata": null, - "option_id": "test-option", + "description": "Winter sale for VIP customers.", + "ends_at": null, + "id": "pl", + "name": "VIP winter sale", + "starts_at": null, + "status": "active", + "type": "sale", "updated_at": Any, - "value": "Default variant", - "variant_id": "test-variant", }, - ], - "origin_country": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price", - "max_quantity": null, - "min_quantity": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant", - }, - ], - "product": Any, - "product_id": "test-product", - "sku": "test-sku", - "title": "Test variant", - "upc": "test-upc", - "updated_at": Any, - "weight": null, - "width": null, - }, - ], + "price_list_id": "pl", + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, + ], + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, } `; -exports[`/store/variants lists by title 1`] = ` +exports[`/store/variants GET /store/variants advanced pricing lists by title 1`] = ` Object { "variants": Array [ Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -147,6 +178,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -156,6 +188,7 @@ Object { "id": Any, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, @@ -174,3 +207,151 @@ Object { ], } `; + +exports[`/store/variants GET /store/variants advanced pricing sets additional prices correctly when querying a variant 1`] = ` +Object { + "variant": Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": 80, + "calculated_price_type": "sale", + "created_at": Any, + "deleted_at": null, + "ean": "test-ean", + "height": null, + "hs_code": null, + "id": "test-variant", + "inventory_quantity": 10, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "options": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "id": "test-variant-option", + "metadata": null, + "option_id": "test-option", + "updated_at": Any, + "value": "Default variant", + "variant_id": "test-variant", + }, + ], + "origin_country": null, + "original_price": 100, + "prices": Array [ + Object { + "amount": 100, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price", + "max_quantity": null, + "min_quantity": null, + "price_list": null, + "price_list_id": null, + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, + Object { + "amount": 80, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price-discount", + "max_quantity": null, + "min_quantity": null, + "price_list": Object { + "created_at": Any, + "deleted_at": null, + "description": "Winter sale for VIP customers.", + "ends_at": null, + "id": "pl", + "name": "VIP winter sale", + "starts_at": null, + "status": "active", + "type": "sale", + "updated_at": Any, + }, + "price_list_id": "pl", + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, + ], + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, +} +`; + +exports[`/store/variants GET /store/variants includes default relations 1`] = ` +Object { + "variants": Array [ + Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": null, + "created_at": Any, + "deleted_at": null, + "ean": "test-ean", + "height": null, + "hs_code": null, + "id": "test-variant", + "inventory_quantity": 10, + "length": null, + "manage_inventory": true, + "material": null, + "metadata": null, + "mid_code": null, + "options": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "id": "test-variant-option", + "metadata": null, + "option_id": "test-option", + "updated_at": Any, + "value": "Default variant", + "variant_id": "test-variant", + }, + ], + "origin_country": null, + "original_price": null, + "prices": Array [ + Object { + "amount": 100, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price", + "max_quantity": null, + "min_quantity": null, + "price_list": null, + "price_list_id": null, + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, + ], + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, + ], +} +`; diff --git a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap index 9037ba8821..d998ac8523 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap @@ -102,7 +102,7 @@ Object { ], "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, - "status": "draft", + "status": "published", "subtitle": null, "tags": Array [ Object { @@ -130,6 +130,7 @@ Object { Object { "allow_backorder": false, "barcode": "test-barcode", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -155,6 +156,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -164,11 +166,37 @@ Object { "id": "test-price", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, "variant_id": "test-variant", }, + Object { + "amount": 80, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price-discount", + "max_quantity": null, + "min_quantity": null, + "price_list": Object { + "created_at": Any, + "deleted_at": null, + "description": "Winter sale for VIP customers.", + "ends_at": null, + "id": "pl", + "name": "VIP winter sale", + "starts_at": null, + "status": "active", + "type": "sale", + "updated_at": Any, + }, + "price_list_id": "pl", + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant", + }, ], "product_id": "test-product", "sku": "test-sku", @@ -181,6 +209,7 @@ Object { Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean2", @@ -206,6 +235,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -215,11 +245,37 @@ Object { "id": "test-price2", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, "variant_id": "test-variant_2", }, + Object { + "amount": 80, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price2-discount", + "max_quantity": null, + "min_quantity": null, + "price_list": Object { + "created_at": Any, + "deleted_at": null, + "description": "Winter sale for VIP customers.", + "ends_at": null, + "id": "pl", + "name": "VIP winter sale", + "starts_at": null, + "status": "active", + "type": "sale", + "updated_at": Any, + }, + "price_list_id": "pl", + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant_2", + }, ], "product_id": "test-product", "sku": "test-sku2", @@ -232,6 +288,7 @@ Object { Object { "allow_backorder": false, "barcode": "test-barcode 1", + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": "test-ean1", @@ -257,6 +314,7 @@ Object { }, ], "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -266,11 +324,37 @@ Object { "id": "test-price1", "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, "variant_id": "test-variant_1", }, + Object { + "amount": 80, + "created_at": Any, + "currency_code": "usd", + "deleted_at": null, + "id": "test-price1-discount", + "max_quantity": null, + "min_quantity": null, + "price_list": Object { + "created_at": Any, + "deleted_at": null, + "description": "Winter sale for VIP customers.", + "ends_at": null, + "id": "pl", + "name": "VIP winter sale", + "starts_at": null, + "status": "active", + "type": "sale", + "updated_at": Any, + }, + "price_list_id": "pl", + "region_id": null, + "updated_at": Any, + "variant_id": "test-variant_1", + }, ], "product_id": "test-product", "sku": "test-sku1", @@ -300,6 +384,7 @@ Object { Object { "allow_backorder": false, "barcode": null, + "calculated_price": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -313,6 +398,7 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "original_price": null, "prices": Array [ Object { "amount": 100, @@ -322,6 +408,7 @@ Object { "id": Any, "max_quantity": null, "min_quantity": null, + "price_list": null, "price_list_id": null, "region_id": null, "updated_at": Any, diff --git a/integration-tests/api/__tests__/store/cart.js b/integration-tests/api/__tests__/store/cart.js index f4e2f068c0..f8206715e4 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -5,6 +5,8 @@ const { GiftCard, Cart, CustomShippingOption, + PriceList, + MoneyAmount, } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") @@ -90,20 +92,51 @@ describe("/store/carts", () => { it("creates a cart with items", async () => { await productSeeder(dbConnection) + + const yesterday = ((today) => + new Date(today.setDate(today.getDate() - 1)))(new Date()) + const tomorrow = ((today) => + new Date(today.setDate(today.getDate() + 1)))(new Date()) + + const priceList1 = await dbConnection.manager.create(PriceList, { + id: "pl_current", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: yesterday, + ends_at: tomorrow, + }) + + await dbConnection.manager.save(priceList1) + + const ma_sale_1 = dbConnection.manager.create(MoneyAmount, { + variant_id: "test-variant-sale", + currency_code: "usd", + amount: 800, + price_list_id: "pl_current", + }) + + await dbConnection.manager.save(ma_sale_1) + const api = useApi() - const response = await api.post("/store/carts", { - items: [ - { - variant_id: "test-variant_1", - quantity: 1, - }, - { - variant_id: "test-variant_2", - quantity: 2, - }, - ], - }) + const response = await api + .post("/store/carts", { + items: [ + { + variant_id: "test-variant_1", + quantity: 1, + }, + { + variant_id: "test-variant-sale", + quantity: 2, + }, + ], + }) + .catch((err) => console.log(err)) + + response.data.cart.items.sort((a, b) => a.quantity - b.quantity) expect(response.status).toEqual(200) expect(response.data.cart.items).toEqual([ @@ -112,8 +145,9 @@ describe("/store/carts", () => { quantity: 1, }), expect.objectContaining({ - variant_id: "test-variant_2", + variant_id: "test-variant-sale", quantity: 2, + unit_price: 800, }), ]) @@ -155,6 +189,122 @@ describe("/store/carts", () => { }) }) + describe("POST /store/carts/:id/line-items", () => { + beforeEach(async () => { + try { + await cartSeeder(dbConnection) + await swapSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + await doAfterEach() + }) + + it("adds line item to cart", async () => { + const api = useApi() + + // Add standard line item to cart + const response = await api + .post( + "/store/carts/test-cart/line-items", + { + variant_id: "test-variant-quantity", + quantity: 1, + }, + { withCredentials: true } + ) + .catch((err) => console.log(err)) + + expect(response.data.cart.items).toEqual([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 1000, + variant_id: "test-variant-quantity", + quantity: 1, + }), + ]) + }) + + it("adds line item to cart time limited sale", async () => { + const api = useApi() + + // Add standard line item to cart + const response = await api + .post( + "/store/carts/test-cart/line-items", + { + variant_id: "test-variant-sale", + quantity: 1, + }, + { withCredentials: true } + ) + .catch((err) => console.log(err)) + + expect(response.data.cart.items).toEqual([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 800, + variant_id: "test-variant-sale", + quantity: 1, + }), + ]) + }) + + it("adds line item with quantity to cart with quantity discount", async () => { + const api = useApi() + + // Add standard line item to cart + const response = await api + .post( + "/store/carts/test-cart/line-items", + { + variant_id: "test-variant-quantity", + quantity: 90, + }, + { withCredentials: true } + ) + .catch((err) => console.log(err)) + + expect(response.data.cart.items).toEqual([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 800, + variant_id: "test-variant-quantity", + quantity: 90, + }), + ]) + }) + + it("adds line item with quantity to cart with quantity discount no ceiling", async () => { + const api = useApi() + + // Add standard line item to cart + const response = await api + .post( + "/store/carts/test-cart/line-items", + { + variant_id: "test-variant-quantity", + quantity: 900, + }, + { withCredentials: true } + ) + .catch((err) => console.log(err)) + + expect(response.data.cart.items).toEqual([ + expect.objectContaining({ + cart_id: "test-cart", + unit_price: 700, + variant_id: "test-variant-quantity", + quantity: 900, + }), + ]) + }) + }) + describe("POST /store/carts/:id", () => { beforeEach(async () => { try { @@ -270,6 +420,46 @@ describe("/store/carts", () => { expect(response.status).toEqual(200) }) + it("updates prices when cart customer id is updated", async () => { + const api = useApi() + + const beforeUpdate = await api + .get(`/store/carts/test-cart-3`) + .catch((error) => console.log(error)) + + expect(beforeUpdate.data.cart.items[0].unit_price).toEqual(8000) + + const response = await api + .post("/store/carts/test-cart-3", { + customer_id: "test-customer-2", + }) + .catch((error) => console.log(error)) + + expect(response.status).toEqual(200) + expect(response.data.cart.items[0].unit_price).toEqual(500) + }) + + it("updates prices when cart region id is updated", async () => { + const api = useApi() + + const beforeUpdate = await api + .get(`/store/carts/test-cart-3`) + .catch((error) => console.log(error)) + + expect(beforeUpdate.data.cart.items[0].unit_price).toEqual(8000) + expect(beforeUpdate.data.cart.region_id).toEqual("test-region") + + const response = await api + .post("/store/carts/test-cart-3", { + region_id: "test-region-multiple", + }) + .catch((error) => console.log(error)) + + expect(response.status).toEqual(200) + expect(response.data.cart.region_id).toEqual("test-region-multiple") + expect(response.data.cart.items[0].unit_price).toEqual(700) + }) + it("updates address using string id", async () => { const api = useApi() @@ -377,7 +567,7 @@ describe("/store/carts", () => { await api.post(`/store/carts/test-cart-2/complete-cart`) } catch (error) { expect(error.response.data).toMatchSnapshot({ - code: "not_allowed", + type: "not_allowed", message: "Cart has already been completed", code: "cart_incompatible_state", }) diff --git a/integration-tests/api/__tests__/store/product-variants.js b/integration-tests/api/__tests__/store/product-variants.js index 636bffce17..b11bcfbfbc 100644 --- a/integration-tests/api/__tests__/store/product-variants.js +++ b/integration-tests/api/__tests__/store/product-variants.js @@ -1,3 +1,4 @@ +const { ProductVariant } = require("@medusajs/medusa") const path = require("path") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") @@ -5,6 +6,7 @@ const { initDb, useDb } = require("../../../helpers/use-db") const { simpleProductFactory } = require("../../factories") const productSeeder = require("../../helpers/product-seeder") +const storeProductSeeder = require("../../helpers/store-product-seeder") jest.setTimeout(30000) describe("/store/variants", () => { let medusaProcess @@ -22,46 +24,100 @@ describe("/store/variants", () => { medusaProcess.kill() }) - beforeEach(async () => { - try { - await productSeeder(dbConnection) + describe("GET /store/variants", () => { + beforeEach(async () => { + try { + await productSeeder(dbConnection) - await simpleProductFactory( - dbConnection, - { - title: "prod", - variants: [ - { - title: "test1", - inventory_quantity: 10, - }, - { - title: "test2", - inventory_quantity: 12, - }, - ], - }, - 100 - ) - } catch (err) { - console.log(err) - throw err - } - }) + await simpleProductFactory( + dbConnection, + { + title: "prod", + variants: [ + { + title: "test1", + inventory_quantity: 10, + }, + { + title: "test2", + inventory_quantity: 12, + }, + ], + }, + 100 + ) + } catch (err) { + console.log(err) + throw err + } + }) - afterEach(async () => { - const db = useDb() - await db.teardown() - }) + afterEach(async () => { + const db = useDb() + await db.teardown() + }) - it("includes default relations", async () => { - const api = useApi() + it("includes default relations", async () => { + const api = useApi() - const response = await api.get("/store/variants?ids=test-variant") + const response = await api.get("/store/variants?ids=test-variant") - expect(response.data).toMatchSnapshot({ - variants: [ - { + expect(response.data).toMatchSnapshot({ + variants: [ + { + allow_backorder: false, + barcode: "test-barcode", + created_at: expect.any(String), + deleted_at: null, + ean: "test-ean", + height: null, + hs_code: null, + id: "test-variant", + inventory_quantity: 10, + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + origin_country: null, + product_id: "test-product", + sku: "test-sku", + title: "Test variant", + upc: "test-upc", + updated_at: expect.any(String), + weight: null, + width: null, + options: [ + { + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + prices: [ + { + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 100, + currency_code: "usd", + deleted_at: null, + id: "test-price", + region_id: null, + variant_id: "test-variant", + }, + ], + product: expect.any(Object), + }, + ], + }) + }) + + it("/test-variant", async () => { + const api = useApi() + + const response = await api.get("/store/variants/test-variant") + + expect(response.data).toMatchSnapshot({ + variant: { allow_backorder: false, barcode: "test-barcode", created_at: expect.any(String), @@ -106,103 +162,230 @@ describe("/store/variants", () => { }, ], product: expect.any(Object), - options: [ - { created_at: expect.any(String), updated_at: expect.any(String) }, - ], }, - ], + }) }) }) - it("lists by title", async () => { - const api = useApi() + describe("GET /store/variants advanced pricing", () => { + beforeEach(async () => { + try { + await storeProductSeeder(dbConnection) - const response = await api.get( - "/store/variants?title[]=test1&title[]=test2&inventory_quantity[gt]=10" - ) - expect(response.data).toMatchSnapshot({ - variants: [ - { - id: expect.any(String), - title: "test2", + await simpleProductFactory( + dbConnection, + { + title: "prod", + variants: [ + { + title: "test1", + inventory_quantity: 10, + }, + { + title: "test2", + inventory_quantity: 12, + }, + ], + }, + 100 + ) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("/test-variant with currency code", async () => { + const api = useApi() + + const response = await api.get( + "/store/variants/test-variant?currency_code=usd" + ) + + expect(response.data).toMatchSnapshot({ + variant: { + allow_backorder: false, + barcode: "test-barcode", created_at: expect.any(String), + deleted_at: null, + ean: "test-ean", + height: null, + hs_code: null, + id: "test-variant", + inventory_quantity: 10, + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + origin_country: null, + product_id: "test-product", + sku: "test-sku", + title: "Test variant", + upc: "test-upc", updated_at: expect.any(String), + weight: null, + width: null, options: [ { created_at: expect.any(String), updated_at: expect.any(String), - id: expect.any(String), - option_id: expect.any(String), - variant_id: expect.any(String), }, ], prices: [ { - id: expect.any(String), - variant_id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 100, + currency_code: "usd", + deleted_at: null, + id: "test-price", + region_id: null, + min_quantity: null, + max_quantity: null, + price_list_id: null, + variant_id: "test-variant", + }, + { + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + deleted_at: null, + price_list_id: "pl", + id: "test-price-discount", + region_id: null, + variant_id: "test-variant", + price_list: { + created_at: expect.any(String), + updated_at: expect.any(String), + id: "pl", + }, + }, + ], + product: expect.any(Object), + }, + }) + }) + + it("sets additional prices correctly when querying a variant", async () => { + const api = useApi() + + const response = await api + .get("/store/variants/test-variant?cart_id=test-cart") + .catch((error) => console.log(error)) + + expect(response.data).toMatchSnapshot({ + variant: { + id: "test-variant", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku", + ean: "test-ean", + upc: "test-upc", + barcode: "test-barcode", + product_id: "test-product", + allow_backorder: false, + created_at: expect.any(String), + deleted_at: null, + height: null, + hs_code: null, + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + origin_country: null, + updated_at: expect.any(String), + weight: null, + width: null, + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", created_at: expect.any(String), updated_at: expect.any(String), }, ], + original_price: 100, + calculated_price: 80, + prices: [ + { + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 100, + currency_code: "usd", + deleted_at: null, + id: "test-price", + region_id: null, + variant_id: "test-variant", + }, + { + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + deleted_at: null, + price_list_id: "pl", + id: "test-price-discount", + region_id: null, + variant_id: "test-variant", + price_list: { + created_at: expect.any(String), + updated_at: expect.any(String), + id: "pl", + }, + }, + ], product: expect.any(Object), - product_id: expect.any(String), }, - ], + }) }) - }) - it("/test-variant", async () => { - const api = useApi() + it("lists by title", async () => { + const api = useApi() - const response = await api.get("/store/variants/test-variant") + const response = await api + .get( + "/store/variants?title[]=test1&title[]=test2&inventory_quantity[gt]=10" + ) + .catch((err) => console.log(err)) - expect(response.data).toMatchSnapshot({ - variant: { - allow_backorder: false, - barcode: "test-barcode", - created_at: expect.any(String), - deleted_at: null, - ean: "test-ean", - height: null, - hs_code: null, - id: "test-variant", - inventory_quantity: 10, - length: null, - manage_inventory: true, - material: null, - metadata: null, - mid_code: null, - origin_country: null, - product_id: "test-product", - sku: "test-sku", - title: "Test variant", - upc: "test-upc", - updated_at: expect.any(String), - weight: null, - width: null, - options: [ + expect(response.data).toMatchSnapshot({ + variants: [ { + id: expect.any(String), + title: "test2", created_at: expect.any(String), updated_at: expect.any(String), + options: [ + { + created_at: expect.any(String), + updated_at: expect.any(String), + id: expect.any(String), + option_id: expect.any(String), + variant_id: expect.any(String), + }, + ], + prices: [ + { + id: expect.any(String), + variant_id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + product: expect.any(Object), + product_id: expect.any(String), }, ], - prices: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 100, - currency_code: "usd", - deleted_at: null, - id: "test-price", - region_id: null, - min_quantity: null, - max_quantity: null, - price_list_id: null, - variant_id: "test-variant", - }, - ], - product: expect.any(Object), - }, + }) }) }) }) diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index c35a69527e..c5a80658fe 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -1,4 +1,3 @@ -const { Product } = require("@medusajs/medusa") const path = require("path") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") @@ -201,17 +200,26 @@ describe("/store/products", () => { }) expect(response.status).toEqual(200) + expect(response.data.products.length).toEqual(5) expect(response.data.products).toEqual([ expect.objectContaining({ - id: "giftcard", + id: "test-product1", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product_filtering_2", + collection_id: "test-collection2", }), expect.objectContaining({ id: "test-product_filtering_1", collection_id: "test-collection1", }), expect.objectContaining({ - id: "test-product_filtering_2", - collection_id: "test-collection2", + id: "giftcard", }), ]) @@ -223,12 +231,111 @@ describe("/store/products", () => { }) }) + describe("list params", () => { + beforeEach(async () => { + try { + await productSeeder(dbConnection) + await adminSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("Includes Additional prices when queried with a cart id", async () => { + const api = useApi() + + const response = await api + .get("/store/products?cart_id=test-cart") + .catch((err) => { + console.log(err) + }) + + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: "test-product1", + collection_id: "test-collection", + }), + expect.objectContaining({ + id: "test-product", + collection_id: "test-collection", + variants: [ + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price2", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price2-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price1", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price1-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), + ], + }), + expect.objectContaining({ + id: "test-product_filtering_2", + collection_id: "test-collection2", + }), + expect.objectContaining({ + id: "test-product_filtering_1", + collection_id: "test-collection1", + }), + expect.objectContaining({ + id: "giftcard", + }), + ]) + }) + }) + describe("list params", () => { beforeEach(async () => { try { await adminSeeder(dbConnection) - const p1 = await simpleProductFactory( + await simpleProductFactory( dbConnection, { title: "testprod", @@ -238,7 +345,7 @@ describe("/store/products", () => { 11 ) - const p2 = await simpleProductFactory( + await simpleProductFactory( dbConnection, { title: "testprod3", @@ -332,6 +439,8 @@ describe("/store/products", () => { height: null, hs_code: null, origin_country: null, + calculated_price: null, + original_price: null, barcode: "test-barcode", product_id: "test-product", created_at: expect.any(String), @@ -349,13 +458,30 @@ describe("/store/products", () => { amount: 100, currency_code: "usd", deleted_at: null, - id: "test-price", - region_id: null, min_quantity: null, max_quantity: null, price_list_id: null, + id: "test-price", + region_id: null, variant_id: "test-variant", }, + { + id: "test-price-discount", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant", + price_list: { + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, ], }, { @@ -375,6 +501,8 @@ describe("/store/products", () => { hs_code: null, origin_country: null, barcode: null, + calculated_price: null, + original_price: null, product_id: "test-product", created_at: expect.any(String), updated_at: expect.any(String), @@ -386,18 +514,33 @@ describe("/store/products", () => { ], prices: [ { + id: "test-price2", created_at: expect.any(String), updated_at: expect.any(String), amount: 100, currency_code: "usd", - deleted_at: null, - id: "test-price2", - region_id: null, - min_quantity: null, - max_quantity: null, price_list_id: null, + deleted_at: null, + region_id: null, variant_id: "test-variant_2", }, + { + id: "test-price2-discount", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant_2", + price_list: { + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, ], }, { @@ -416,6 +559,8 @@ describe("/store/products", () => { height: null, hs_code: null, origin_country: null, + calculated_price: null, + original_price: null, barcode: "test-barcode 1", product_id: "test-product", created_at: expect.any(String), @@ -428,18 +573,35 @@ describe("/store/products", () => { ], prices: [ { + id: "test-price1", created_at: expect.any(String), updated_at: expect.any(String), amount: 100, currency_code: "usd", - deleted_at: null, - id: "test-price1", - region_id: null, min_quantity: null, max_quantity: null, price_list_id: null, + deleted_at: null, + region_id: null, variant_id: "test-variant_1", }, + { + id: "test-price1-discount", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant_1", + price_list: { + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + }, ], }, ], diff --git a/integration-tests/api/helpers/cart-seeder.js b/integration-tests/api/helpers/cart-seeder.js index da7eb55b83..116a58709e 100644 --- a/integration-tests/api/helpers/cart-seeder.js +++ b/integration-tests/api/helpers/cart-seeder.js @@ -14,6 +14,8 @@ const { LineItem, Payment, PaymentSession, + CustomerGroup, + PriceList, } = require("@medusajs/medusa") module.exports = async (connection, data = {}) => { @@ -52,6 +54,8 @@ module.exports = async (connection, data = {}) => { tax_rate: 0, }) + await manager.save(r) + // Region with multiple countries const regionWithMultipleCoutries = manager.create(Region, { id: "test-region-multiple", @@ -240,7 +244,7 @@ module.exports = async (connection, data = {}) => { is_disabled: false, starts_at: tenDaysAgo, ends_at: tenDaysFromToday, - valid_duration: "P1M", //one month + valid_duration: "P1M", // one month }) DynamicDiscount.regions = [r] @@ -256,11 +260,21 @@ module.exports = async (connection, data = {}) => { email: "test@email.com", }) - await manager.insert(Customer, { + const c2 = await manager.create(Customer, { id: "test-customer-2", email: "test-2@email.com", }) + const cg = await manager.create(CustomerGroup, { + id: "cgroup", + name: "customer group", + }) + + await manager.save(cg) + + c2.groups = [cg] + await manager.save(c2) + await manager.insert(Customer, { id: "some-customer", email: "some-customer@email.com", @@ -299,6 +313,28 @@ module.exports = async (connection, data = {}) => { data: {}, }) + const priceList = await manager.create(PriceList, { + id: "pl", + name: "VIP winter sale", + description: "Winter sale for VIP customers.", + type: "sale", + status: "active", + }) + + await manager.save(priceList) + + const priceList1 = await manager.create(PriceList, { + id: "pl_current", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: tenDaysFromToday, + }) + + await manager.save(priceList1) + await manager.insert(Product, { id: "giftcard-product", title: "Giftcard", @@ -328,6 +364,32 @@ module.exports = async (connection, data = {}) => { options: [{ id: "test-option", title: "Size" }], }) + await manager.insert(ProductVariant, { + id: "test-variant-quantity", + title: "test variant", + product_id: "test-product", + inventory_quantity: 1000, + options: [ + { + option_id: "test-option", + value: "Size", + }, + ], + }) + + await manager.insert(ProductVariant, { + id: "test-variant-sale", + title: "test variant", + product_id: "test-product", + inventory_quantity: 1000, + options: [ + { + option_id: "test-option", + value: "Size", + }, + ], + }) + await manager.insert(ProductVariant, { id: "test-variant", title: "test variant", @@ -357,14 +419,60 @@ module.exports = async (connection, data = {}) => { const ma = manager.create(MoneyAmount, { variant_id: "test-variant", currency_code: "usd", + type: "default", amount: 1000, }) - await manager.save(ma) + const ma_sale = manager.create(MoneyAmount, { + variant_id: "test-variant-sale", + currency_code: "usd", + amount: 1000, + }) + await manager.save(ma_sale) + + const ma_sale_1 = manager.create(MoneyAmount, { + variant_id: "test-variant-sale", + currency_code: "usd", + amount: 800, + price_list_id: "pl_current", + }) + + await manager.save(ma_sale_1) + + const ma_quantity = manager.create(MoneyAmount, { + variant_id: "test-variant-quantity", + currency_code: "usd", + type: "default", + amount: 1000, + }) + await manager.save(ma_quantity) + + const ma_quantity_1 = manager.create(MoneyAmount, { + variant_id: "test-variant-quantity", + currency_code: "usd", + type: "sale", + min_quantity: 10, + max_quantity: 100, + amount: 800, + }) + + await manager.save(ma_quantity_1) + + const ma_quantity_2 = manager.create(MoneyAmount, { + variant_id: "test-variant-quantity", + currency_code: "usd", + type: "sale", + min_quantity: 100, + amount: 700, + }) + + await manager.save(ma_quantity_2) + const ma2 = manager.create(MoneyAmount, { variant_id: "test-variant-2", currency_code: "usd", + type: "default", amount: 8000, }) @@ -373,6 +481,7 @@ module.exports = async (connection, data = {}) => { const ma3 = manager.create(MoneyAmount, { variant_id: "giftcard-denom", currency_code: "usd", + type: "default", amount: 1000, }) @@ -533,4 +642,67 @@ module.exports = async (connection, data = {}) => { cart_id: "test-cart-3", }) await manager.save(li2) + + const cart4 = manager.create(Cart, { + id: "test-cart-3", + email: "some-customer@email.com", + shipping_address: { + id: "test-shipping-address", + first_name: "lebron", + country_code: "us", + }, + region_id: "test-region", + currency_code: "usd", + completed_at: null, + items: [], + }) + await manager.save(cart4) + + await manager.insert(ProductVariant, { + id: "test-variant-sale-cg", + title: "test variant", + product_id: "test-product", + inventory_quantity: 1000, + options: [ + { + option_id: "test-option", + value: "Size", + }, + ], + }) + + const ma_cg = manager.create(MoneyAmount, { + variant_id: "test-variant-sale-cg", + currency_code: "usd", + amount: 1000, + }) + await manager.save(ma_cg) + + const ma_sale_cg = manager.create(MoneyAmount, { + variant_id: "test-variant-sale-cg", + currency_code: "usd", + price_list_id: "pl", + amount: 500, + }) + await manager.save(ma_sale_cg) + + const ma_sale_cg_new_region = manager.create(MoneyAmount, { + variant_id: "test-variant-sale-cg", + region_id: "test-region-multiple", + currency_code: "eur", + amount: 700, + }) + await manager.save(ma_sale_cg_new_region) + + const li3 = await manager.create(LineItem, { + id: "test-item3", + title: "Line Item", + description: "Line Item Desc", + thumbnail: "https://test.js/1234", + unit_price: 8000, + quantity: 1, + variant_id: "test-variant-sale-cg", + cart_id: "test-cart-3", + }) + await manager.save(li3) } diff --git a/integration-tests/api/helpers/price-selection-seeder.js b/integration-tests/api/helpers/price-selection-seeder.js new file mode 100644 index 0000000000..00d939f760 --- /dev/null +++ b/integration-tests/api/helpers/price-selection-seeder.js @@ -0,0 +1,883 @@ +const { + Customer, + CustomerGroup, + ShippingProfile, + Product, + ProductVariant, + ProductCollection, + ProductOption, + Region, + Cart, + PriceList, +} = require("@medusajs/medusa") + +module.exports = async (connection, data = {}) => { + const yesterday = ((today) => new Date(today.setDate(today.getDate() - 1)))( + new Date() + ) + const tomorrow = ((today) => new Date(today.setDate(today.getDate() + 1)))( + new Date() + ) + const tenDaysAgo = ((today) => new Date(today.setDate(today.getDate() - 10)))( + new Date() + ) + const tenDaysFromToday = ((today) => + new Date(today.setDate(today.getDate() + 10)))(new Date()) + + const manager = connection.manager + + const defaultProfile = await manager.findOne(ShippingProfile, { + type: "default", + }) + + await manager.insert(Region, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + await manager.insert(Region, { + id: "test-region-1", + name: "Test Region no prices", + currency_code: "usd", + tax_rate: 0, + }) + + await manager.insert(Region, { + id: "test-region-2", + name: "Test Region no prices", + currency_code: "dkk", + tax_rate: 0, + }) + + const coll = manager.create(ProductCollection, { + id: "test-collection", + handle: "test-collection", + title: "Test collection", + }) + await manager.save(coll) + + const customer5 = await manager.create(Customer, { + id: "test-customer-5", + email: "test5@email.com", + first_name: "John", + last_name: "Deere", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + await manager.save(customer5) + + const customer6 = await manager.create(Customer, { + id: "test-customer-6", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + email: "test6@email.com", + has_account: true, + }) + await manager.save(customer6) + + const customer7 = await manager.create(Customer, { + id: "test-customer-7", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + email: "test7@email.com", + has_account: true, + }) + await manager.save(customer7) + + const c_group_5 = await manager.create(CustomerGroup, { + id: "test-group-5", + name: "test-group-5", + }) + await manager.save(c_group_5) + + const c_group_6 = await manager.create(CustomerGroup, { + id: "test-group-6", + name: "test-group-6", + }) + await manager.save(c_group_6) + + customer5.groups = [c_group_5] + await manager.save(customer5) + + customer6.groups = [c_group_6] + await manager.save(customer6) + + customer7.groups = [c_group_5, c_group_6] + await manager.save(customer7) + + const priceList = await manager.create(PriceList, { + id: "pl", + name: "VIP winter sale", + description: "Winter sale for VIP customers.", + type: "sale", + status: "active", + }) + + await manager.save(priceList) + + const priceList1 = await manager.create(PriceList, { + id: "pl_1", + name: "VIP winter sale", + description: "Winter sale for VIP customers.", + type: "sale", + status: "active", + }) + + priceList1.customer_groups = [c_group_5] + + await manager.save(priceList1) + + const priceList2 = await manager.create(PriceList, { + id: "pl_2", + name: "VVIP winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + }) + + priceList2.customer_groups = [c_group_6] + + await manager.save(priceList2) + + const priceList3 = await manager.create(PriceList, { + id: "pl_expired", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: yesterday, + }) + + await manager.save(priceList3) + + const priceList4 = await manager.create(PriceList, { + id: "pl_upcoming", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tomorrow, + ends_at: tenDaysFromToday, + }) + + await manager.save(priceList4) + + const priceList5 = await manager.create(PriceList, { + id: "pl_current", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: tenDaysFromToday, + }) + + await manager.save(priceList5) + + const priceList6 = await manager.create(PriceList, { + id: "pl_current_1", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: tomorrow, + }) + + await manager.save(priceList6) + + const priceList7 = await manager.create(PriceList, { + id: "pl_upcoming-customer", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tomorrow, + ends_at: tenDaysFromToday, + }) + + priceList7.customer_groups = [c_group_5] + + await manager.save(priceList7) + + const priceList8 = await manager.create(PriceList, { + id: "pl_current-customer", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: tenDaysFromToday, + }) + + priceList8.customer_groups = [c_group_5] + + await manager.save(priceList8) + + const priceList9 = await manager.create(PriceList, { + id: "pl_expired-customer", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: yesterday, + }) + + priceList9.customer_groups = [c_group_5] + + await manager.save(priceList9) + + const p1 = await manager.create(Product, { + id: "test-product", + handle: "test-product", + title: "Test product", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p1) + + await manager.save(ProductOption, { + id: "test-option", + title: "test-option", + product_id: "test-product", + }) + + const variant4 = await manager.create(ProductVariant, { + id: "test-variant", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku", + ean: "test-ean", + status: "published", + upc: "test-upc", + product_id: "test-product", + prices: [ + { + id: "test-price", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl_1", + }, + { + id: "test-price1", + region_id: "test-region", + currency_code: "usd", + amount: 120, + }, + { + id: "test-price2", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl_2", + }, + { + id: "test-price3", + region_id: "test-region", + currency_code: "usd", + amount: 110, + price_list_id: "pl", + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variant4) + + const p2 = await manager.create(Product, { + id: "test-product-quantity", + handle: "test-product-quantity", + title: "Test product", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p2) + + await manager.save(ProductOption, { + id: "test-option", + title: "test-option", + product_id: "test-product-quantity", + }) + + const variant_quantity = await manager.create(ProductVariant, { + id: "test-variant-quantity", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-quantity", + ean: "test-ean-quantity", + status: "published", + upc: "test-upc-quantity", + product_id: "test-product-quantity", + prices: [ + { + id: "test-price-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl", + min_quantity: 10, + max_quantity: 100, + }, + { + id: "test-price1-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl", + min_quantity: 101, + max_quantity: 1000, + }, + { + id: "test-price1-quantity-group", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_1", + }, + { + id: "test-price2-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl", + max_quantity: 9, + }, + { + id: "test-price3-quantity-expired", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_expired", + }, + { + id: "test-price3-quantity-future", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_upcoming", + }, + { + id: "test-price3-quantity-now", + region_id: "test-region", + currency_code: "usd", + amount: 140, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_current", + }, + { + id: "test-price3-quantity-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variant_quantity) + + const p3 = await manager.create(Product, { + id: "test-product-sale", + handle: "test-product-sale", + title: "Test product sale", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p3) + + await manager.save(ProductOption, { + id: "test-option-sale", + title: "test-option sale", + product_id: "test-product-sale", + }) + + const variant_sale = await manager.create(ProductVariant, { + id: "test-variant-sale", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-sale", + ean: "test-ean-sale", + status: "published", + upc: "test-upc-sale", + product_id: "test-product-sale", + prices: [ + { + id: "test-price-sale", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl_expired", + }, + { + id: "test-price1-sale", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl_current", + }, + { + id: "test-price2-sale", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl_upcoming", + }, + { + id: "test-price2-sale-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variant_sale) + + const p4 = await manager.create(Product, { + id: "test-product-sale-overlap", + handle: "test-product-sale-overlap", + title: "Test product sale", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p4) + + await manager.save(ProductOption, { + id: "test-option-sale-overlap", + title: "test-option sale", + product_id: "test-product-sale-overlap", + }) + + const variant_sale_overlap = await manager.create(ProductVariant, { + id: "test-variant-sale-overlap", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-sale-overlap", + ean: "test-ean-sale-overlap", + status: "published", + upc: "test-upc-sale-overlap", + product_id: "test-product-sale-overlap", + prices: [ + { + id: "test-price-sale-overlap-1", + region_id: "test-region", + currency_code: "usd", + amount: 140, + price_list_id: "pl_current_1", + }, + { + id: "test-price1-sale-overlap", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl_current", + }, + { + id: "test-price2-sale-overlap-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }, + ], + options: [ + { + id: "test-variant-option-overlap", + value: "Default variant", + option_id: "test-option-sale-overlap", + }, + ], + }) + + await manager.save(variant_sale_overlap) + + const multiRegionProduct = await manager.create(Product, { + id: "test-product-multi-region", + handle: "test-product-multi-region", + title: "Test product", + profile_id: defaultProfile.id, + description: "test-product-multi-region-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(multiRegionProduct) + + await manager.save(ProductOption, { + id: "test-option-multi-region", + title: "test-option", + product_id: "test-product-multi-region", + }) + + const variant_multi_region = await manager.create(ProductVariant, { + id: "test-variant-multi-region", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-multi-region", + ean: "test-ean-multi-region", + status: "published", + upc: "test-upc-multi-region", + product_id: "test-product-multi-region", + prices: [ + { + id: "test-price-region-1", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl", + }, + { + id: "test-price1-region-1", + region_id: "test-region", + currency_code: "usd", + amount: 120, + }, + { + id: "test-price1-region-2", + region_id: "test-region-2", + currency_code: "dkk", + amount: 130, + }, + { + id: "test-price3-region-2", + region_id: "test-region-2", + currency_code: "dkk", + price_list_id: "pl", + amount: 110, + }, + ], + options: [ + { + id: "test-variant-option-multi-region", + value: "Default variant", + option_id: "test-option-multi-region", + }, + ], + }) + + await manager.save(variant_multi_region) + + const p5 = await manager.create(Product, { + id: "test-product-quantity-customer", + handle: "test-product-quantity-customer", + title: "Test product", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p5) + + await manager.save(ProductOption, { + id: "test-option-quantity-customer", + title: "test-option", + product_id: "test-product-quantity-customer", + }) + + const variant_quantity_customer = await manager.create(ProductVariant, { + id: "test-variant-quantity-customer", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-quantity-customer", + ean: "test-ean-quantity-customer", + status: "published", + upc: "test-upc-quantity-customer", + product_id: "test-product-quantity-customer", + prices: [ + { + id: "test-price-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl", + min_quantity: 10, + max_quantity: 100, + }, + { + id: "test-price1-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl", + min_quantity: 101, + max_quantity: 1000, + }, + { + id: "test-price2-quantity-customer", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl", + max_quantity: 9, + }, + { + id: "test-price2-quantity-customer-group", + region_id: "test-region", + currency_code: "usd", + amount: 100, + max_quantity: 9, + price_list_id: "pl_1", + }, + { + id: "test-price3-quantity-customer-expired", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_expired", + }, + { + id: "test-price3-quantity-customer-future", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_upcoming", + }, + { + id: "test-price3-quantity-customer-now", + region_id: "test-region", + currency_code: "usd", + amount: 140, + min_quantity: 101, + max_quantity: 1000, + price_list_id: "pl_current", + }, + { + id: "test-price3-quantity-customer-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option-quantity-customer", + }, + ], + }) + + await manager.save(variant_quantity_customer) + + const p6 = await manager.create(Product, { + id: "test-product-sale-customer", + handle: "test-product-sale-customer", + title: "Test product sale", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p6) + + await manager.save(ProductOption, { + id: "test-option-sale", + title: "test-option sale", + product_id: "test-product-sale-customer", + }) + + const variant_sale_customer = await manager.create(ProductVariant, { + id: "test-variant-sale-customer", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-sale-customer", + ean: "test-ean-sale-customer", + status: "published", + upc: "test-upc-sale-customer", + product_id: "test-product-sale-customer", + prices: [ + { + id: "test-price-sale-customer", + region_id: "test-region", + currency_code: "usd", + amount: 120, + price_list_id: "pl_expired-customer", + }, + { + id: "test-price1-sale-customer", + region_id: "test-region", + currency_code: "usd", + amount: 100, + price_list_id: "pl_current-customer", + }, + { + id: "test-price2-sale-customer", + region_id: "test-region", + currency_code: "usd", + amount: 130, + price_list_id: "pl_upcoming-customer", + }, + { + id: "test-price2-sale-customer-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + type: "default", + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variant_sale_customer) + + const p7 = await manager.create(Product, { + id: "test-product-sale-customer-quantity", + handle: "test-product-sale-customer-quantity", + title: "Test product sale", + profile_id: defaultProfile.id, + description: "test-product-description1", + collection_id: "test-collection", + tags: [], + }) + + await manager.save(p7) + + await manager.save(ProductOption, { + id: "test-option-sale", + title: "test-option sale", + product_id: "test-product-sale-customer-quantity", + }) + + const variant_sale_customer_quantity = await manager.create(ProductVariant, { + id: "test-variant-sale-customer-quantity", + inventory_quantity: 10, + title: "Test variant", + sku: "test-sku-sale-customer-quantity", + ean: "test-ean-sale-customer-quantity", + status: "published", + upc: "test-upc-sale-customer-quantity", + product_id: "test-product-sale-customer-quantity", + prices: [ + { + id: "test-price-sale-customer-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 120, + min_quantity: 100, + max_quantity: 1000, + price_list_id: "pl_expired-customer", + }, + { + id: "test-price1-sale-customer-quantity-groups", + region_id: "test-region", + currency_code: "usd", + amount: 100, + max_quantity: 99, + price_list_id: "pl_current-customer", + }, + { + id: "test-price2-sale-customer-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 130, + min_quantity: 500, + max_quantity: 900, + price_list_id: "pl_upcoming-customer", + }, + { + id: "test-price2-sale-customer-quantity-default", + region_id: "test-region", + currency_code: "usd", + amount: 150, + }, + { + id: "test-price1-sale-customer-quantity", + region_id: "test-region", + currency_code: "usd", + amount: 110, + max_quantity: 99, + }, + ], + options: [ + { + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variant_sale_customer_quantity) + + const cart = await manager.create(Cart, { + id: "test-cart", + region_id: "test-region", + currency_code: "usd", + items: [], + }) + + await manager.save(cart) + + const cart_region2 = await manager.create(Cart, { + id: "test-cart-1", + region_id: "test-region-2", + currency_code: "dkk", + items: [], + }) + + await manager.save(cart_region2) + + const cart_region1 = await manager.create(Cart, { + id: "test-cart-2", + region_id: "test-region-1", + currency_code: "usd", + items: [], + }) + + await manager.save(cart_region1) +} diff --git a/integration-tests/api/helpers/product-seeder.js b/integration-tests/api/helpers/product-seeder.js index 9155afaac5..e593651071 100644 --- a/integration-tests/api/helpers/product-seeder.js +++ b/integration-tests/api/helpers/product-seeder.js @@ -17,7 +17,7 @@ module.exports = async (connection, data = {}) => { type: "default", }) - const coll = manager.create(ProductCollection, { + const coll = await manager.create(ProductCollection, { id: "test-collection", handle: "test-collection", title: "Test collection", @@ -25,7 +25,7 @@ module.exports = async (connection, data = {}) => { await manager.save(coll) - const coll1 = manager.create(ProductCollection, { + const coll1 = await manager.create(ProductCollection, { id: "test-collection1", handle: "test-collection1", title: "Test collection 1", @@ -33,7 +33,7 @@ module.exports = async (connection, data = {}) => { await manager.save(coll1) - const coll2 = manager.create(ProductCollection, { + const coll2 = await manager.create(ProductCollection, { id: "test-collection2", handle: "test-collection2", title: "Test collection 2", @@ -41,28 +41,28 @@ module.exports = async (connection, data = {}) => { await manager.save(coll2) - const tag = manager.create(ProductTag, { + const tag = await manager.create(ProductTag, { id: "tag1", value: "123", }) await manager.save(tag) - const tag3 = manager.create(ProductTag, { + const tag3 = await manager.create(ProductTag, { id: "tag3", value: "123", }) await manager.save(tag3) - const tag4 = manager.create(ProductTag, { + const tag4 = await manager.create(ProductTag, { id: "tag4", value: "123", }) await manager.save(tag4) - const type = manager.create(ProductType, { + const type = await manager.create(ProductType, { id: "test-type", value: "test-type", }) @@ -83,7 +83,7 @@ module.exports = async (connection, data = {}) => { tax_rate: 0, }) - const p = manager.create(Product, { + const p = await manager.create(Product, { id: "test-product", handle: "test-product", title: "Test product", @@ -129,6 +129,34 @@ module.exports = async (connection, data = {}) => { await manager.save(variant1) + const sale = await manager.create(ProductVariant, { + id: "test-variant-sale", + inventory_quantity: 10, + title: "Test variant", + variant_rank: 3, + sku: "test-sku-sale", + ean: "test-ean-sale", + upc: "test-upc-sale", + barcode: "test-barcode-sale", + product_id: "test-product", + prices: [ + { + id: "test-price-sale", + currency_code: "usd", + amount: 1000, + }, + ], + options: [ + { + id: "test-variant-option-sale", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(sale) + const variant2 = await manager.create(ProductVariant, { id: "test-variant_1", inventory_quantity: 10, @@ -172,7 +200,7 @@ module.exports = async (connection, data = {}) => { await manager.save(variant3) - const p1 = manager.create(Product, { + const p1 = await manager.create(Product, { id: "test-product1", handle: "test-product1", title: "Test product1", @@ -203,6 +231,7 @@ module.exports = async (connection, data = {}) => { region_id: "test-region", currency_code: "usd", amount: 100, + type: "default", }, ], options: [ @@ -225,7 +254,9 @@ module.exports = async (connection, data = {}) => { ean: "test-ean4", upc: "test-upc4", product_id: "test-product1", - prices: [{ id: "test-price4", currency_code: "usd", amount: 100 }], + prices: [ + { id: "test-price4", currency_code: "usd", amount: 100, type: "default" }, + ], options: [ { id: "test-variant-option-4", @@ -237,7 +268,7 @@ module.exports = async (connection, data = {}) => { await manager.save(variant5) - const product1 = manager.create(Product, { + const product1 = await manager.create(Product, { id: "test-product_filtering_1", handle: "test-product_filtering_1", title: "Test product filtering 1", @@ -251,7 +282,7 @@ module.exports = async (connection, data = {}) => { await manager.save(product1) - const product2 = manager.create(Product, { + const product2 = await manager.create(Product, { id: "test-product_filtering_2", handle: "test-product_filtering_2", title: "Test product filtering 2", @@ -265,7 +296,7 @@ module.exports = async (connection, data = {}) => { await manager.save(product2) - const product3 = manager.create(Product, { + const product3 = await manager.create(Product, { id: "test-product_filtering_3", handle: "test-product_filtering_3", title: "Test product filtering 3", @@ -279,7 +310,7 @@ module.exports = async (connection, data = {}) => { await manager.save(product3) - const product4 = manager.create(Product, { + const product4 = await manager.create(Product, { id: "test-product_filtering_4", handle: "test-product_filtering_4", title: "Test product filtering 4", diff --git a/integration-tests/api/helpers/store-product-seeder.js b/integration-tests/api/helpers/store-product-seeder.js index c0b232085c..2290465c1a 100644 --- a/integration-tests/api/helpers/store-product-seeder.js +++ b/integration-tests/api/helpers/store-product-seeder.js @@ -8,11 +8,45 @@ const { ShippingProfile, ProductVariant, Image, + Cart, + PriceList, } = require("@medusajs/medusa") module.exports = async (connection, data = {}) => { const manager = connection.manager + const yesterday = ((today) => new Date(today.setDate(today.getDate() - 1)))( + new Date() + ) + const tomorrow = ((today) => new Date(today.setDate(today.getDate() + 1)))( + new Date() + ) + const tenDaysAgo = ((today) => new Date(today.setDate(today.getDate() - 10)))( + new Date() + ) + + const priceList = await manager.create(PriceList, { + id: "pl", + name: "VIP winter sale", + description: "Winter sale for VIP customers.", + type: "sale", + status: "active", + }) + + await manager.save(priceList) + + const priceList1 = await manager.create(PriceList, { + id: "pl_expired", + name: "Past winter sale", + description: "Winter sale for key accounts.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: yesterday, + }) + + await manager.save(priceList1) + const defaultProfile = await manager.findOne(ShippingProfile, { type: "default", }) @@ -83,12 +117,20 @@ module.exports = async (connection, data = {}) => { tax_rate: 0, }) + await manager.insert(Cart, { + id: "test-cart", + region_id: "test-region", + currency_code: "usd", + items: [], + }) + const p = manager.create(Product, { id: "test-product", handle: "test-product", title: "Test product", profile_id: defaultProfile.id, description: "test-product-description", + status: "published", collection_id: "test-collection", type: { id: "test-type", value: "test-type" }, tags: [ @@ -117,7 +159,21 @@ module.exports = async (connection, data = {}) => { upc: "test-upc", barcode: "test-barcode", product_id: "test-product", - prices: [{ id: "test-price", currency_code: "usd", amount: 100 }], + prices: [ + { id: "test-price", currency_code: "usd", type: "default", amount: 100 }, + { + id: "test-price-discount", + currency_code: "usd", + amount: 80, + price_list_id: "pl", + }, + { + id: "test-price-discount-expired", + currency_code: "usd", + amount: 70, + price_list_id: "pl_expired", + }, + ], options: [ { id: "test-variant-option", @@ -139,7 +195,21 @@ module.exports = async (connection, data = {}) => { upc: "test-upc1", barcode: "test-barcode 1", product_id: "test-product", - prices: [{ id: "test-price1", currency_code: "usd", amount: 100 }], + prices: [ + { id: "test-price1", currency_code: "usd", type: "default", amount: 100 }, + { + id: "test-price1-discount", + currency_code: "usd", + amount: 80, + price_list_id: "pl", + }, + { + id: "test-price1-discount-expired", + currency_code: "usd", + amount: 70, + price_list_id: "pl_expired", + }, + ], options: [ { id: "test-variant-option-1", @@ -160,7 +230,21 @@ module.exports = async (connection, data = {}) => { ean: "test-ean2", upc: "test-upc2", product_id: "test-product", - prices: [{ id: "test-price2", currency_code: "usd", amount: 100 }], + prices: [ + { id: "test-price2", currency_code: "usd", type: "default", amount: 100 }, + { + id: "test-price2-discount", + currency_code: "usd", + amount: 80, + price_list_id: "pl", + }, + { + id: "test-price2-discount-expired", + currency_code: "usd", + amount: 70, + price_list_id: "pl_expired", + }, + ], options: [ { id: "test-variant-option-2", @@ -176,6 +260,7 @@ module.exports = async (connection, data = {}) => { id: "test-product1", handle: "test-product1", title: "Test product1", + status: "published", profile_id: defaultProfile.id, description: "test-product-description1", collection_id: "test-collection", diff --git a/packages/medusa-plugin-add-ons/src/services/__mocks__/product-variant.js b/packages/medusa-plugin-add-ons/src/services/__mocks__/product-variant.js index 080ae98b32..d7f69a54ad 100644 --- a/packages/medusa-plugin-add-ons/src/services/__mocks__/product-variant.js +++ b/packages/medusa-plugin-add-ons/src/services/__mocks__/product-variant.js @@ -74,9 +74,9 @@ export const ProductVariantServiceMock = { } return Promise.resolve(undefined) }), - getRegionPrice: jest.fn().mockImplementation((variantId, regionId) => { + getRegionPrice: jest.fn().mockImplementation((variantId, context) => { if (variantId === IdMap.getId("test-variant-1")) { - if (regionId === IdMap.getId("world")) { + if (context.regionId === IdMap.getId("world")) { return Promise.resolve(10) } else { return Promise.resolve(20) diff --git a/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js b/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js index bc7e71e2e9..e30940be2a 100644 --- a/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js +++ b/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js @@ -58,7 +58,7 @@ class AddOnLineItemService extends BaseService { let unitPrice = await this.productVariantService_.getRegionPrice( variant._id, - region._id + {regionId: region._id} ) const addOnPrices = await Promise.all( diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.ts b/packages/medusa/src/api/routes/admin/products/update-variant.ts index c0629bb9e8..0d7b814d92 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/update-variant.ts @@ -8,8 +8,10 @@ import { IsString, ValidateNested, } from "class-validator" +import { MedusaError } from "medusa-core-utils" import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { ProductService, ProductVariantService } from "../../../../services" +import { PriceSelectionParams } from "../../../../types/price-selection" import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant" import { validator } from "../../../../utils/validator" @@ -130,6 +132,8 @@ export default async (req, res) => { req.body ) + const validatedQueryParams = await validator(PriceSelectionParams, req.query) + const productService: ProductService = req.scope.resolve("productService") const productVariantService: ProductVariantService = req.scope.resolve( "productVariantService" @@ -140,9 +144,12 @@ export default async (req, res) => { ...validated, }) + await productVariantService.retrieve(variant_id) + const product = await productService.retrieve(id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, + ...validatedQueryParams, }) res.json({ product }) diff --git a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js index 7a8bc083d9..62057b0812 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js @@ -21,7 +21,13 @@ describe("Get product by id", () => { expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( IdMap.getId("product1"), - { relations: defaultStoreProductsRelations } + { + relations: defaultStoreProductsRelations, + cart_id: undefined, + currency_code: undefined, + region_id: undefined, + include_discount_prices: true, + } ) }) @@ -51,7 +57,13 @@ describe("Get product by id", () => { it("endpoint called with defaultRelations", () => { expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( IdMap.getId("variantsWithPrices"), - { relations: defaultStoreProductsRelations } + { + relations: defaultStoreProductsRelations, + cart_id: undefined, + currency_code: undefined, + region_id: undefined, + include_discount_prices: true, + } ) }) diff --git a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js index 3f092d8fa8..fa838cb708 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js @@ -19,7 +19,15 @@ describe("GET /store/products", () => { expect(ProductServiceMock.listAndCount).toHaveBeenCalledTimes(1) expect(ProductServiceMock.listAndCount).toHaveBeenCalledWith( { status: ["published"] }, - { relations: defaultStoreProductsRelations, skip: 0, take: 100 } + { + relations: defaultStoreProductsRelations, + skip: 0, + take: 100, + cart_id: undefined, + currency_code: undefined, + region_id: undefined, + include_discount_prices: true, + } ) }) @@ -42,7 +50,15 @@ describe("GET /store/products", () => { expect(ProductServiceMock.listAndCount).toHaveBeenCalledTimes(1) expect(ProductServiceMock.listAndCount).toHaveBeenCalledWith( { is_giftcard: true, status: ["published"] }, - { relations: defaultStoreProductsRelations, skip: 0, take: 100 } + { + relations: defaultStoreProductsRelations, + skip: 0, + take: 100, + cart_id: undefined, + currency_code: undefined, + region_id: undefined, + include_discount_prices: true, + } ) }) }) diff --git a/packages/medusa/src/api/routes/store/products/get-product.ts b/packages/medusa/src/api/routes/store/products/get-product.ts index 9974d3c711..52d6180a2c 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.ts +++ b/packages/medusa/src/api/routes/store/products/get-product.ts @@ -1,5 +1,7 @@ import { defaultStoreProductsRelations } from "." import { ProductService } from "../../../../services" +import { PriceSelectionParams } from "../../../../types/price-selection" +import { validator } from "../../../../utils/validator" /** * @oas [get] /products/{id} @@ -23,9 +25,18 @@ import { ProductService } from "../../../../services" export default async (req, res) => { const { id } = req.params + const validated = await validator(PriceSelectionParams, req.query) + + const customer_id = req.user?.customer_id + const productService: ProductService = req.scope.resolve("productService") const product = await productService.retrieve(id, { relations: defaultStoreProductsRelations, + cart_id: validated.cart_id, + customer_id: customer_id, + region_id: validated.region_id, + currency_code: validated.currency_code, + include_discount_prices: true, }) res.json({ product }) diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index 29c398a564..944227f163 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -11,6 +11,7 @@ import { omit, pickBy } from "lodash" import { defaultStoreProductsRelations } from "." import { ProductService } from "../../../../services" import { DateComparisonOperator } from "../../../../types/common" +import { PriceSelectionParams } from "../../../../types/price-selection" import { validator } from "../../../../utils/validator" import { IsType } from "../../../../utils/validators/is-type" import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" @@ -68,6 +69,9 @@ export default async (req, res) => { "expand", "limit", "offset", + "cart_id", + "region_id", + "currency_code", ]) // get only published products for store endpoint @@ -92,6 +96,11 @@ export default async (req, res) => { : defaultStoreProductsRelations, skip: validated.offset, take: validated.limit, + cart_id: validated.cart_id, + region_id: validated.region_id, + currency_code: validated.currency_code, + customer_id: req.user?.customer_id, + include_discount_prices: true, } const [products, count] = await productService.listAndCount( @@ -107,7 +116,7 @@ export default async (req, res) => { }) } -export class StoreGetProductsPaginationParams { +export class StoreGetProductsPaginationParams extends PriceSelectionParams { @IsString() @IsOptional() fields?: string diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js index 5090457c54..4f8a13cc2f 100644 --- a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js +++ b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js @@ -16,6 +16,10 @@ describe("Get variant by id", () => { expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith("1", { relations: ["prices", "options"], + cart_id: undefined, + currency_code: undefined, + region_id: undefined, + include_discount_prices: true, }) }) diff --git a/packages/medusa/src/api/routes/store/variants/get-variant.ts b/packages/medusa/src/api/routes/store/variants/get-variant.ts index 9af345bb5c..424eff61aa 100644 --- a/packages/medusa/src/api/routes/store/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/store/variants/get-variant.ts @@ -1,5 +1,7 @@ import { defaultStoreVariantRelations } from "." import ProductVariantService from "../../../../services/product-variant" +import { PriceSelectionParams } from "../../../../types/price-selection" +import { validator } from "../../../../utils/validator" /** * @oas [get] /variants/{variant_id} @@ -23,12 +25,21 @@ import ProductVariantService from "../../../../services/product-variant" export default async (req, res) => { const { id } = req.params + const validated = await validator(PriceSelectionParams, req.query) + const variantService: ProductVariantService = req.scope.resolve( "productVariantService" ) + const customer_id = req.user?.customer_id + const variant = await variantService.retrieve(id, { relations: defaultStoreVariantRelations, + cart_id: validated.cart_id, + customer_id: customer_id, + region_id: validated.region_id, + currency_code: validated.currency_code, + include_discount_prices: true, }) res.json({ variant }) diff --git a/packages/medusa/src/api/routes/store/variants/list-variants.ts b/packages/medusa/src/api/routes/store/variants/list-variants.ts index 5cfd175a43..06f9634f2a 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/store/variants/list-variants.ts @@ -7,6 +7,7 @@ import ProductVariantService from "../../../../services/product-variant" import { validator } from "../../../../utils/validator" import { IsType } from "../../../../utils/validators/is-type" import { NumericalComparisonOperator } from "../../../../types/common" +import { PriceSelectionParams } from "../../../../types/price-selection" /** * @oas [get] /variants @@ -41,12 +42,19 @@ export default async (req, res) => { expandFields = expand.split(",") } + const customer_id = req.user?.customer_id + const listConfig = { relations: expandFields.length ? expandFields : defaultStoreVariantRelations, skip: offset, take: limit, + cart_id: validated.cart_id, + region_id: validated.region_id, + currency_code: validated.currency_code, + customer_id: customer_id, + include_discount_prices: true, } const filterableFields: FilterableProductVariantProps = omit(validated, [ @@ -54,6 +62,9 @@ export default async (req, res) => { "limit", "offset", "expand", + "cart_id", + "region_id", + "currency_code", ]) if (validated.ids) { @@ -68,7 +79,7 @@ export default async (req, res) => { res.json({ variants }) } -export class StoreGetVariantsParams { +export class StoreGetVariantsParams extends PriceSelectionParams { @IsOptional() @IsInt() @Type(() => Number) diff --git a/packages/medusa/src/interfaces/price-selection-strategy.ts b/packages/medusa/src/interfaces/price-selection-strategy.ts index da42bda0f6..9abf2ce4ff 100644 --- a/packages/medusa/src/interfaces/price-selection-strategy.ts +++ b/packages/medusa/src/interfaces/price-selection-strategy.ts @@ -1,6 +1,5 @@ import { EntityManager } from "typeorm" import { MoneyAmount } from ".." -import { MoneyAmountRepository } from "../repositories/money-amount" import { PriceListType } from "../types/price-list" export interface IPriceSelectionStrategy { diff --git a/packages/medusa/src/models/money-amount.ts b/packages/medusa/src/models/money-amount.ts index 2b208b9330..5f62e1171b 100644 --- a/packages/medusa/src/models/money-amount.ts +++ b/packages/medusa/src/models/money-amount.ts @@ -8,7 +8,7 @@ import { JoinColumn, ManyToOne, PrimaryColumn, - UpdateDateColumn + UpdateDateColumn, } from "typeorm" import { ulid } from "ulid" import { resolveDbType } from "../utils/db-aware-column" @@ -41,11 +41,10 @@ export class MoneyAmount { @Column({ nullable: true }) price_list_id: string | null - @ManyToOne( - () => PriceList, - (priceList) => priceList.prices, - { cascade: true, onDelete: "CASCADE" } - ) + @ManyToOne(() => PriceList, (priceList) => priceList.prices, { + cascade: true, + onDelete: "CASCADE", + }) @JoinColumn({ name: "price_list_id" }) price_list: PriceList | null diff --git a/packages/medusa/src/models/price-list.ts b/packages/medusa/src/models/price-list.ts index f01a9340a8..f60f07917a 100644 --- a/packages/medusa/src/models/price-list.ts +++ b/packages/medusa/src/models/price-list.ts @@ -8,7 +8,7 @@ import { ManyToMany, OneToMany, PrimaryColumn, - UpdateDateColumn + UpdateDateColumn, } from "typeorm" import { ulid } from "ulid" import { PriceListStatus, PriceListType } from "../types/price-list" @@ -53,18 +53,14 @@ export class PriceList { referencedColumnName: "id", }, }) - @ManyToMany( - () => CustomerGroup, - (cg) => cg.price_lists, - { onDelete: "CASCADE" } - ) + @ManyToMany(() => CustomerGroup, (cg) => cg.price_lists, { + onDelete: "CASCADE", + }) customer_groups: CustomerGroup[] - @OneToMany( - () => MoneyAmount, - (moneyAmount) => moneyAmount.price_list, - { onDelete: "CASCADE" } - ) + @OneToMany(() => MoneyAmount, (moneyAmount) => moneyAmount.price_list, { + onDelete: "CASCADE", + }) prices: MoneyAmount[] @CreateDateColumn({ type: resolveDbType("timestamptz") }) diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 7faf0ca453..63713651b7 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -1,4 +1,5 @@ import partition from "lodash/partition" +import { MedusaError } from "medusa-core-utils" import { Brackets, EntityRepository, @@ -130,7 +131,7 @@ export class MoneyAmountRepository extends Repository { "price_list", "ma.price_list_id = price_list.id " ) - .where({ variant_id: variant_id }) // "ma.variant_id = :variant_id", + .where({ variant_id: variant_id }) .andWhere("(ma.price_list_id is null or price_list.status = 'active')") .andWhere( "(price_list is null or price_list.ends_at is null OR price_list.ends_at > :date) ", diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index edaa677af1..2a700f6bfa 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1040,6 +1040,19 @@ describe("CartService", () => { return this }, } + + const priceSelectionStrat = { + calculateVariantPrice: async (variantId, context) => { + if (variantId === IdMap.getId("fail")) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Money amount for variant with id ${variantId} in region ${context.region_id} does not exist` + ) + } else { + return { calculatedPrice: 100 } + } + }, + } const cartService = new CartService({ manager: MockManager, paymentProviderService, @@ -1050,6 +1063,7 @@ describe("CartService", () => { lineItemService, productVariantService, eventBusService, + priceSelectionStrategy: priceSelectionStrat, }) beforeEach(() => { @@ -1316,7 +1330,7 @@ describe("CartService", () => { jest.clearAllMocks() }) - let cartService = new CartService({}) + const cartService = new CartService({}) it("given a cart with custom shipping options and a shipping option id corresponding to a custom shipping option, then it should return a custom shipping option", async () => { const cartCSO = [ diff --git a/packages/medusa/src/services/__tests__/product-variant.js b/packages/medusa/src/services/__tests__/product-variant.js index 41dc539b8d..eb018a564c 100644 --- a/packages/medusa/src/services/__tests__/product-variant.js +++ b/packages/medusa/src/services/__tests__/product-variant.js @@ -3,7 +3,7 @@ import ProductVariantService from "../product-variant" const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } @@ -18,9 +18,33 @@ describe("ProductVariantService", () => { return Promise.resolve({ id: IdMap.getId("ironman") }) }, }) + + const cartRepository = MockRepository({ + findOne: (data) => { + return Promise.resolve({}) + }, + }) + + const priceSelectionStrat = { + calculateVariantPrice: (variantId, context) => { + return { + originalPrice: null, + calculatedPrice: null, + prices: [], + } + }, + } + const priceSelectionStrategy = { + withTransaction: (manager) => { + return priceSelectionStrat + }, + } + const productVariantService = new ProductVariantService({ manager: MockManager, productVariantRepository, + cartRepository, + priceSelectionStrategy, }) beforeEach(async () => { @@ -227,6 +251,29 @@ describe("ProductVariantService", () => { }) describe("update", () => { + const cartRepository = MockRepository({ + findOne: (data) => { + return Promise.resolve({}) + }, + }) + + const priceSelectionStrat = { + calculateVariantPrice: (variantId, context) => { + return { + originalPrice: null, + calculatedPrice: null, + calculatedPriceType: undefined, + prices: [], + } + }, + } + + const priceSelectionStrategy = { + withTransaction: (manager) => { + return priceSelectionStrat + }, + } + const productVariantRepository = MockRepository({ findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }), }) @@ -246,6 +293,8 @@ describe("ProductVariantService", () => { moneyAmountRepository, productVariantRepository, productOptionValueRepository, + cartRepository, + priceSelectionStrategy, }) productVariantService.updateOptionValue = jest @@ -464,13 +513,13 @@ describe("ProductVariantService", () => { describe("getRegionPrice", () => { const regionService = { - retrieve: function() { + retrieve: function () { return Promise.resolve({ id: IdMap.getId("california"), name: "California", }) }, - withTransaction: function() { + withTransaction: function () { return this }, } @@ -498,11 +547,35 @@ describe("ProductVariantService", () => { }, }) + const cartRepository = MockRepository({ + findOne: (data) => { + return Promise.resolve({}) + }, + }) + + const priceSelectionStrat = { + calculateVariantPrice: (variantId, context) => { + return Promise.resolve({ + originalPrice: null, + calculatedPrice: 1000, + prices: [], + }) + }, + } + + const priceSelectionStrategy = { + withTransaction: (manager) => { + return priceSelectionStrat + }, + } + const productVariantService = new ProductVariantService({ manager: MockManager, eventBusService, regionService, moneyAmountRepository, + cartRepository, + priceSelectionStrategy, }) beforeEach(async () => { diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index 9bb7c2b406..bbfc3bb278 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -3,7 +3,7 @@ import ProductService from "../product" const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } @@ -54,7 +54,7 @@ describe("ProductService", () => { describe("create", () => { const productRepository = MockRepository({ - create: product => ({ + create: (product) => ({ id: IdMap.getId("ironman"), title: "Suit", options: [], @@ -71,7 +71,7 @@ describe("ProductService", () => { const productTagRepository = MockRepository({ findOne: () => Promise.resolve(undefined), - create: data => { + create: (data) => { if (data.value === "title") { return { id: "tag-1", value: "title" } } @@ -83,16 +83,16 @@ describe("ProductService", () => { }) const productTypeRepository = MockRepository({ findOne: () => Promise.resolve(undefined), - create: data => { + create: (data) => { return { id: "type", value: "type1" } }, }) const productCollectionService = { - withTransaction: function() { + withTransaction: function () { return this }, - retrieve: id => + retrieve: (id) => Promise.resolve({ id: IdMap.getId("cat"), title: "Suits" }), } @@ -223,7 +223,7 @@ describe("ProductService", () => { const productTypeRepository = MockRepository({ findOne: () => Promise.resolve(undefined), - create: data => { + create: (data) => { return { id: "type", value: "type1" } }, }) @@ -231,7 +231,7 @@ describe("ProductService", () => { const productVariantRepository = MockRepository() const productVariantService = { - withTransaction: function() { + withTransaction: function () { return this }, update: (variant, update) => { @@ -243,7 +243,7 @@ describe("ProductService", () => { } const productTagRepository = MockRepository({ - findOne: data => { + findOne: (data) => { if (data.where.value === "test") { return Promise.resolve({ id: IdMap.getId("test"), value: "test" }) } @@ -253,6 +253,25 @@ describe("ProductService", () => { }, }) + const cartRepository = MockRepository({ + findOne: (data) => { + return Promise.resolve({}) + }, + }) + + const priceSelectionStrategy = { + withTransaction: (manager) => { + return this + }, + calculateVariantPrice: (variantId, context) => { + return { + originalPrice: null, + calculatedPrice: null, + prices: [], + } + }, + } + const productService = new ProductService({ manager: MockManager, productRepository, @@ -261,6 +280,8 @@ describe("ProductService", () => { productTagRepository, productTypeRepository, eventBusService, + cartRepository, + priceSelectionStrategy, }) beforeEach(() => { @@ -440,7 +461,7 @@ describe("ProductService", () => { describe("addOption", () => { const productRepository = MockRepository({ - findOneWithRelations: query => + findOneWithRelations: (query) => Promise.resolve({ id: IdMap.getId("ironman"), options: [{ title: "Color" }], @@ -449,7 +470,7 @@ describe("ProductService", () => { }) const productVariantService = { - withTransaction: function() { + withTransaction: function () { return this }, addOptionValue: jest.fn(), @@ -513,7 +534,7 @@ describe("ProductService", () => { describe("reorderVariants", () => { const productRepository = MockRepository({ - findOneWithRelations: query => + findOneWithRelations: (query) => Promise.resolve({ id: IdMap.getId("ironman"), variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }], @@ -571,7 +592,7 @@ describe("ProductService", () => { describe("reorderOptions", () => { const productRepository = MockRepository({ - findOneWithRelations: query => + findOneWithRelations: (query) => Promise.resolve({ id: IdMap.getId("ironman"), options: [ @@ -637,7 +658,7 @@ describe("ProductService", () => { describe("updateOption", () => { const productRepository = MockRepository({ - findOneWithRelations: query => + findOneWithRelations: (query) => Promise.resolve({ id: IdMap.getId("ironman"), options: [ @@ -712,7 +733,7 @@ describe("ProductService", () => { describe("deleteOption", () => { const productRepository = MockRepository({ - findOneWithRelations: query => + findOneWithRelations: (query) => Promise.resolve({ id: IdMap.getId("ironman"), variants: [ @@ -751,7 +772,7 @@ describe("ProductService", () => { }) const productOptionRepository = MockRepository({ - findOne: query => { + findOne: (query) => { if (query.where.id === IdMap.getId("material")) { return undefined } diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index c8d993feea..45a543dc93 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1,5 +1,9 @@ import _ from "lodash" -import { EntityManager, DeepPartial } from "typeorm" +import { + EntityManager, + DeepPartial, + AlreadyHasActiveConnectionError, +} from "typeorm" import { MedusaError, Validator } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" @@ -38,6 +42,7 @@ import GiftCardService from "./gift-card" import TotalsService from "./totals" import InventoryService from "./inventory" import CustomShippingOptionService from "./custom-shipping-option" +import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy" type CartConstructorProps = { manager: EntityManager @@ -59,6 +64,7 @@ type CartConstructorProps = { totalsService: TotalsService inventoryService: InventoryService customShippingOptionService: CustomShippingOptionService + priceSelectionStrategy: IPriceSelectionStrategy } type TotalsConfig = { @@ -94,6 +100,7 @@ class CartService extends BaseService { private paymentSessionRepository_: typeof PaymentSessionRepository private inventoryService_: InventoryService private customShippingOptionService_: CustomShippingOptionService + private priceSelectionStrategy_: IPriceSelectionStrategy constructor({ manager, @@ -115,6 +122,7 @@ class CartService extends BaseService { paymentSessionRepository, inventoryService, customShippingOptionService, + priceSelectionStrategy, }: CartConstructorProps) { super() @@ -137,6 +145,7 @@ class CartService extends BaseService { this.inventoryService_ = inventoryService this.customShippingOptionService_ = customShippingOptionService this.taxProviderService_ = taxProviderService + this.priceSelectionStrategy_ = priceSelectionStrategy } withTransaction(transactionManager: EntityManager): CartService { @@ -164,6 +173,7 @@ class CartService extends BaseService { giftCardService: this.giftCardService_, inventoryService: this.inventoryService_, customShippingOptionService: this.customShippingOptionService_, + priceSelectionStrategy: this.priceSelectionStrategy_, }) cloned.transactionManager_ = transactionManager @@ -701,12 +711,6 @@ class CartService extends BaseService { ], }) - if (typeof update.region_id !== "undefined") { - const countryCode = - (update.country_code || update.shipping_address?.country_code) ?? null - await this.setRegion_(cart, update.region_id, countryCode) - } - if (typeof update.customer_id !== "undefined") { await this.updateCustomerId_(cart, update.customer_id) } else { @@ -718,6 +722,19 @@ class CartService extends BaseService { } } + if (typeof update.region_id !== "undefined") { + const countryCode = + (update.country_code || update.shipping_address?.country_code) ?? null + await this.setRegion_(cart, update.region_id, countryCode) + } + + if ( + typeof update.customer_id !== "undefined" || + typeof update.region_id !== "undefined" + ) { + await this.updateUnitPrices(cart, update.region_id, update.customer_id) + } + const addrRepo = manager.getCustomRepository(this.addressRepository_) if ("shipping_address_id" in update || "shipping_address" in update) { let address: string | Partial
| undefined @@ -1169,7 +1186,10 @@ class CartService extends BaseService { * this could be IP address or similar for fraud handling. * @return the resulting cart */ - async authorizePayment(cartId: string, context: Record = {}): Promise { + async authorizePayment( + cartId: string, + context: Record = {} + ): Promise { return this.atomicPhase_(async (manager: EntityManager) => { const cartRepository = manager.getCustomRepository(this.cartRepository_) @@ -1592,6 +1612,57 @@ class CartService extends BaseService { return customOption } + async updateUnitPrices( + cart: Cart, + regionId?: string, + customer_id?: string + ): Promise { + // If the cart contains items, we update the price of the items + // to match the updated region or customer id (keeping the old + // value if it exists) + if (cart.items.length) { + const region = await this.regionService_.retrieve( + regionId || cart.region_id, + { + relations: ["countries"], + } + ) + + cart.items = await Promise.all( + cart.items + .map(async (item) => { + const availablePrice = await this.priceSelectionStrategy_ + .calculateVariantPrice(item.variant_id, { + region_id: region.id, + currency_code: region.currency_code, + quantity: item.quantity, + customer_id: customer_id || cart.customer_id, + include_discount_prices: true, + }) + .catch(() => undefined) + + if ( + availablePrice !== undefined && + availablePrice.calculatedPrice !== null + ) { + return this.lineItemService_ + .withTransaction(this.transactionManager_) + .update(item.id, { + has_shipping: false, + unit_price: availablePrice.calculatedPrice, + }) + } else { + await this.lineItemService_ + .withTransaction(this.transactionManager_) + .delete(item.id) + return null + } + }) + .filter(Boolean) + ) + } + } + /** * Set's the region of a cart. * @param cart - the cart to set region on @@ -1619,34 +1690,6 @@ class CartService extends BaseService { cart.region = region cart.region_id = region.id - // If the cart contains items we want to change the unit_price field of each - // item to correspond to the price given in the region - if (cart.items.length) { - cart.items = await Promise.all( - cart.items - .map(async (item) => { - const availablePrice = await this.productVariantService_ - .getRegionPrice(item.variant_id, regionId) - .catch(() => undefined) - - if (availablePrice !== undefined) { - return this.lineItemService_ - .withTransaction(this.transactionManager_) - .update(item.id, { - has_shipping: false, - unit_price: availablePrice, - }) - } else { - await this.lineItemService_ - .withTransaction(this.transactionManager_) - .delete(item.id) - return null - } - }) - .filter(Boolean) - ) - } - /* * When changing the region you are changing the set of countries that your * cart can be shipped to so we need to make sure that the current shipping diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index 9f484cac18..9ba0200e7f 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -143,6 +143,7 @@ class LineItemService extends BaseService { .withTransaction(manager) .retrieve(variantId, { relations: ["product"], + include_discount_prices: true, }) const region = await this.regionService_ @@ -164,7 +165,12 @@ class LineItemService extends BaseService { } else { price = await this.productVariantService_ .withTransaction(manager) - .getRegionPrice(variant.id, region.id) + .getRegionPrice(variant.id, { + regionId: region.id, + quantity: quantity, + customer_id: undefined, + include_discount_prices: true, + }) } const toCreate = { diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 734fbc450c..afd3d46366 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -1,10 +1,16 @@ +import { JsonWebTokenError } from "jsonwebtoken" import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { Brackets, EntityManager, ILike, SelectQueryBuilder } from "typeorm" -import { MoneyAmount } from ".." +import { + IPriceSelectionStrategy, + PriceSelectionContext, +} from "../interfaces/price-selection-strategy" +import { MoneyAmount } from "../models/money-amount" import { Product } from "../models/product" import { ProductOptionValue } from "../models/product-option-value" import { ProductVariant } from "../models/product-variant" +import { CartRepository } from "../repositories/cart" import { MoneyAmountRepository } from "../repositories/money-amount" import { ProductRepository } from "../repositories/product" import { ProductOptionValueRepository } from "../repositories/product-option-value" @@ -18,6 +24,7 @@ import { FindConfig } from "../types/common" import { CreateProductVariantInput, FilterableProductVariantProps, + GetRegionPriceContext, ProductVariantPrice, UpdateProductVariantInput, } from "../types/product-variant" @@ -38,8 +45,10 @@ class ProductVariantService extends BaseService { private productRepository_: typeof ProductRepository private eventBus_: EventBusService private regionService_: RegionService + private priceSelectionStrategy_: IPriceSelectionStrategy private moneyAmountRepository_: typeof MoneyAmountRepository private productOptionValueRepository_: typeof ProductOptionValueRepository + private cartRepository_: typeof CartRepository constructor({ manager, @@ -49,6 +58,8 @@ class ProductVariantService extends BaseService { regionService, moneyAmountRepository, productOptionValueRepository, + cartRepository, + priceSelectionStrategy, }) { super() @@ -70,6 +81,10 @@ class ProductVariantService extends BaseService { this.moneyAmountRepository_ = moneyAmountRepository this.productOptionValueRepository_ = productOptionValueRepository + + this.cartRepository_ = cartRepository + + this.priceSelectionStrategy_ = priceSelectionStrategy } withTransaction(transactionManager: EntityManager): ProductVariantService { @@ -85,6 +100,8 @@ class ProductVariantService extends BaseService { regionService: this.regionService_, moneyAmountRepository: this.moneyAmountRepository_, productOptionValueRepository: this.productOptionValueRepository_, + cartRepository: this.cartRepository_, + priceSelectionStrategy: this.priceSelectionStrategy_, }) cloned.transactionManager_ = transactionManager @@ -100,12 +117,21 @@ class ProductVariantService extends BaseService { */ async retrieve( variantId: string, - config: FindConfig = {} + config: FindConfig & PriceSelectionContext = { + include_discount_prices: false, + } ): Promise { const variantRepo = this.manager_.getCustomRepository( this.productVariantRepository_ ) const validatedId = this.validateId_(variantId) + + const priceIndex = config.relations?.indexOf("prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const query = this.buildQuery_({ id: validatedId }, config) const variant = await variantRepo.findOne(query) @@ -116,7 +142,16 @@ class ProductVariantService extends BaseService { ) } - return variant + return priceIndex >= 0 + ? ((await this.setAdditionalPrices( + variant, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + )) as ProductVariant) + : variant } /** @@ -127,11 +162,20 @@ class ProductVariantService extends BaseService { */ async retrieveBySKU( sku: string, - config: FindConfig = {} + config: FindConfig & PriceSelectionContext = { + include_discount_prices: false, + } ): Promise { const variantRepo = this.manager_.getCustomRepository( this.productVariantRepository_ ) + + const priceIndex = config.relations?.indexOf("prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const query = this.buildQuery_({ sku }, config) const variant = await variantRepo.findOne(query) @@ -142,7 +186,16 @@ class ProductVariantService extends BaseService { ) } - return variant + return priceIndex >= 0 + ? ((await this.setAdditionalPrices( + variant, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + )) as ProductVariant) + : variant } /** @@ -254,6 +307,7 @@ class ProductVariantService extends BaseService { * The function will throw, if price updates are attempted. * @param {string | ProductVariant} variantOrVariantId - variant or id of a variant. * @param {object} update - an object with the update values. + * @param {object} config - an object with the config values for returning the variant. * @return {Promise} resolves to the update result. */ async update( @@ -267,7 +321,17 @@ class ProductVariantService extends BaseService { let variant = variantOrVariantId as ProductVariant if (typeof variant === `string`) { - variant = await this.retrieve(variantOrVariantId as string) + const variantRes = await variantRepo.findOne({ + where: { id: variantOrVariantId as string }, + }) + if (typeof variant === "undefined") { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Variant with id ${variantOrVariantId} was not found` + ) + } else { + variant = variantRes as ProductVariant + } } else if (!variant.id) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -291,12 +355,12 @@ class ProductVariantService extends BaseService { } } - if (metadata) { - variant.metadata = this.setMetadata_(variant, metadata) + if (typeof metadata === "object") { + variant.metadata = this.setMetadata_(variant, metadata as object) } if (typeof inventory_quantity === "number") { - variant.inventory_quantity = inventory_quantity + variant.inventory_quantity = inventory_quantity as number } for (const [key, value] of Object.entries(rest)) { @@ -312,6 +376,7 @@ class ProductVariantService extends BaseService { product_id: result.product_id, fields: Object.keys(update), }) + return result }) } @@ -358,41 +423,29 @@ class ProductVariantService extends BaseService { * exists the function will try to use a currency price. If no default * currency price exists the function will throw an error. * @param {string} variantId - the id of the variant to get price from - * @param {string} regionId - the id of the region to get price for + * @param {GetRegionPriceContext} context - context for getting region price * @return {number} the price specific to the region */ - async getRegionPrice(variantId: string, regionId: string): Promise { + async getRegionPrice( + variantId: string, + context: GetRegionPriceContext + ): Promise { return this.atomicPhase_(async (manager: EntityManager) => { - const moneyAmountRepo = manager.getCustomRepository( - this.moneyAmountRepository_ - ) - const region = await this.regionService_ .withTransaction(manager) - .retrieve(regionId) + .retrieve(context.regionId) - // Find region price based on region id - let moneyAmount = await moneyAmountRepo.findOne({ - where: { region_id: regionId, variant_id: variantId }, - }) - - // If no price could be find based on region id, we try to fetch - // based on the region currency code - if (!moneyAmount) { - moneyAmount = await moneyAmountRepo.findOne({ - where: { variant_id: variantId, currency_code: region.currency_code }, + const prices = await this.priceSelectionStrategy_ + .withTransaction(manager) + .calculateVariantPrice(variantId, { + region_id: context.regionId, + currency_code: region.currency_code, + quantity: context.quantity, + customer_id: context.customer_id, + include_discount_prices: !!context.include_discount_prices, }) - } - // Still, if no price is found, we throw - if (!moneyAmount) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `A price for region: ${region.name} could not be found` - ) - } - - return moneyAmount.amount + return prices.calculatedPrice }) } @@ -554,12 +607,23 @@ class ProductVariantService extends BaseService { */ async listAndCount( selector: FilterableProductVariantProps, - config: FindConfig = { relations: [], skip: 0, take: 20 } + config: FindConfig & PriceSelectionContext = { + relations: [], + skip: 0, + take: 20, + include_discount_prices: false, + } ): Promise<[ProductVariant[], number]> { const variantRepo = this.manager_.getCustomRepository( this.productVariantRepository_ ) + const priceIndex = config.relations?.indexOf("prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const { q, query, relations } = this.prepareListQuery_(selector, config) if (q) { @@ -571,10 +635,39 @@ class ProductVariantService extends BaseService { raw.map((i) => i.id), query.withDeleted ?? false ) + if (priceIndex >= 0) { + const res = await this.setAdditionalPrices( + variants, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + return [res as ProductVariant[], count] + } + return [variants, count] } - return await variantRepo.findWithRelationsAndCount(relations, query) + const [variants, count] = await variantRepo.findWithRelationsAndCount( + relations, + query + ) + + if (priceIndex >= 0) { + const res = await this.setAdditionalPrices( + variants, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + return [res as ProductVariant[], count] + } + + return [variants, count] } /** @@ -584,12 +677,22 @@ class ProductVariantService extends BaseService { */ async list( selector: FilterableProductVariantProps, - config: FindConfig = { relations: [], skip: 0, take: 20 } + config: FindConfig & PriceSelectionContext = { + relations: [], + skip: 0, + take: 20, + } ): Promise { const productVariantRepo = this.manager_.getCustomRepository( this.productVariantRepository_ ) + const priceIndex = config.relations?.indexOf("prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + let q: string | undefined if ("q" in selector) { q = selector.q @@ -620,7 +723,18 @@ class ProductVariantService extends BaseService { } } - return await productVariantRepo.find(query) + const variants = await productVariantRepo.find(query) + + return priceIndex >= 0 + ? ((await this.setAdditionalPrices( + variants, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + )) as ProductVariant[]) + : variants } /** @@ -770,6 +884,72 @@ class ProductVariantService extends BaseService { return qb } + + /** + * Set additional prices on a list of variants. + * @param {ProductVariant | ProductVariant[] } variant variant on which to set additional prices + * @param {string} currency_code currency code to fetch prices for + * @param {string} region_id region to fetch prices for + * @param {string} cart_id string of cart to use as a basis for getting currency and region + * @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups + * @param {boolean} include_discount_prices should result include discount pricing + * @return {Promise} A list of variants with variants decorated with "additional_prices" + */ + async setAdditionalPrices( + variant, + currency_code, + region_id, + cart_id, + customer_id, + include_discount_prices = false + ): Promise { + return this.atomicPhase_(async (manager) => { + const cartRepo = manager.getCustomRepository(this.cartRepository_) + + let regionId = region_id + let currencyCode = currency_code + + if (cart_id) { + const cart = await cartRepo.findOne({ + where: { id: cart_id }, + relations: ["region"], + }) + + regionId = cart.region.id + currencyCode = cart.region.currency_code + } + + const variantArray = Array.isArray(variant) ? variant : [variant] + + const priceSelectionStrategy = + this.priceSelectionStrategy_.withTransaction(manager) + + const variantsWithPrices = await Promise.all( + variantArray.map(async (v) => { + const prices = await priceSelectionStrategy.calculateVariantPrice( + v.id, + { + region_id: regionId, + currency_code: currencyCode, + cart_id: cart_id, + customer_id: customer_id, + include_discount_prices: include_discount_prices, + } + ) + + return { + ...v, + prices: prices.prices, + original_price: prices.originalPrice, + calculated_price: prices.calculatedPrice, + calculated_price_type: prices.calculatedPriceType, + } + }) + ) + + return Array.isArray(variant) ? variantsWithPrices : variantsWithPrices[0] + }) + } } export default ProductVariantService diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 553c24edd5..146b2ded6b 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -27,6 +27,8 @@ class ProductService extends BaseService { productTagRepository, imageRepository, searchService, + cartRepository, + priceSelectionStrategy, }) { super() @@ -62,6 +64,12 @@ class ProductService extends BaseService { /** @private @const {SearchService} */ this.searchService_ = searchService + + /** @private @const {CartRepository} */ + this.cartRepository_ = cartRepository + + /** @private @const {IPriceSelectionStrategy} */ + this.priceSelectionStrategy_ = priceSelectionStrategy } withTransaction(transactionManager) { @@ -80,6 +88,8 @@ class ProductService extends BaseService { productTagRepository: this.productTagRepository_, productTypeRepository: this.productTypeRepository_, imageRepository: this.imageRepository_, + cartRepository: this.cartRepository_, + priceSelectionStrategy: this.priceSelectionStrategy_, }) cloned.transactionManager_ = transactionManager @@ -95,10 +105,23 @@ class ProductService extends BaseService { * returned * @return {Promise} the result of the find operation */ - async list(selector = {}, config = { relations: [], skip: 0, take: 20 }) { + async list( + selector = {}, + config = { + relations: [], + skip: 0, + take: 20, + include_discount_prices: false, + } + ) { const productRepo = this.manager_.getCustomRepository( this.productRepository_ ) + const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } const { q, query, relations } = this.prepareListQuery_(selector, config) @@ -112,7 +135,18 @@ class ProductService extends BaseService { ) } - return productRepo.findWithRelations(relations, query) + const products = productRepo.findWithRelations(relations, query) + + return priceIndex > -1 + ? await this.setAdditionalPrices( + products, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + : products } /** @@ -128,12 +162,23 @@ class ProductService extends BaseService { */ async listAndCount( selector = {}, - config = { relations: [], skip: 0, take: 20 } + config = { + relations: [], + skip: 0, + take: 20, + include_discount_prices: false, + } ) { const productRepo = this.manager_.getCustomRepository( this.productRepository_ ) + const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const { q, query, relations } = this.prepareListQuery_(selector, config) if (q) { @@ -148,7 +193,25 @@ class ProductService extends BaseService { return [products, count] } - return await productRepo.findWithRelationsAndCount(relations, query) + const [products, count] = await productRepo.findWithRelationsAndCount( + relations, + query + ) + + if (priceIndex > -1) { + const productsWithAdditionalPrices = await this.setAdditionalPrices( + products, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + + return [productsWithAdditionalPrices, count] + } else { + return [products, count] + } } /** @@ -172,12 +235,18 @@ class ProductService extends BaseService { * query response * @return {Promise} the result of the find one operation. */ - async retrieve(productId, config = {}) { + async retrieve(productId, config = { include_discount_prices: false }) { const productRepo = this.manager_.getCustomRepository( this.productRepository_ ) const validatedId = this.validateId_(productId) + const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const query = { where: { id: validatedId } } if (config.relations && config.relations.length > 0) { @@ -199,7 +268,16 @@ class ProductService extends BaseService { ) } - return product + return priceIndex > -1 + ? await this.setAdditionalPrices( + product, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + : product } /** @@ -214,6 +292,12 @@ class ProductService extends BaseService { this.productRepository_ ) + const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const query = { where: { handle: productHandle } } if (config.relations && config.relations.length > 0) { @@ -235,7 +319,16 @@ class ProductService extends BaseService { ) } - return product + return priceIndex > -1 + ? await this.setAdditionalPrices( + product, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + : product } /** @@ -250,6 +343,12 @@ class ProductService extends BaseService { this.productRepository_ ) + const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && config.relations) { + config.relations = [...config.relations] + config.relations.splice(priceIndex, 1) + } + const query = { where: { external_id: externalId } } if (config.relations && config.relations.length > 0) { @@ -271,7 +370,16 @@ class ProductService extends BaseService { ) } - return product + return priceIndex > -1 + ? await this.setAdditionalPrices( + product, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + : product } /** @@ -535,6 +643,7 @@ class ProductService extends BaseService { } const result = await productRepo.save(product) + await this.eventBus_ .withTransaction(manager) .emit(ProductService.Events.UPDATED, { @@ -821,11 +930,18 @@ class ProductService extends BaseService { * @param {string} productId - the productId to decorate. * @param {string[]} fields - the fields to include. * @param {string[]} expandFields - fields to expand. + * @param {object} config - retrieve config for price calculation. * @return {Product} return the decorated product. */ - async decorate(productId, fields = [], expandFields = []) { + async decorate(productId, fields = [], expandFields = [], config = {}) { const requiredFields = ["id", "metadata"] + const priceIndex = expandFields.indexOf("variants.prices") ?? -1 + if (priceIndex >= 0 && expandFields.length) { + expandFields = [...expandFields] + expandFields.splice(priceIndex, 1) + } + fields = fields.concat(requiredFields) const product = await this.retrieve(productId, { @@ -833,8 +949,16 @@ class ProductService extends BaseService { relations: expandFields, }) - // const final = await this.runDecorators_(decorated) - return product + return priceIndex > -1 + ? await this.setAdditionalPrices( + product, + config.currency_code, + config.region_id, + config.cart_id, + config.customer_id, + config.include_discount_prices + ) + : product } /** @@ -908,6 +1032,80 @@ class ProductService extends BaseService { return qb } + + /** + * Set additional prices on a list of products. + * @param {Product[] | Product} products list of products on which to set additional prices + * @param {string} currency_code currency code to fetch prices for + * @param {string} region_id region to fetch prices for + * @param {string} cart_id string of cart to use as a basis for getting currency and region + * @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups + * @param {boolean} include_discount_prices indication wether or not to include sales prices in result + * @return {Promise} A list of products with variants decorated with "additional_prices" + */ + async setAdditionalPrices( + products, + currency_code, + region_id, + cart_id, + customer_id, + include_discount_prices = false + ) { + return this.atomicPhase_(async (manager) => { + const cartRepo = this.manager_.getCustomRepository(this.cartRepository_) + + let regionId = region_id + let currencyCode = currency_code + + if (cart_id) { + const cart = await cartRepo.findOne({ + where: { id: cart_id }, + relations: ["region"], + }) + + regionId = cart.region.id + currencyCode = cart.region.currency_code + } + + const productArray = Array.isArray(products) ? products : [products] + + const priceSelectionStrategy = + this.priceSelectionStrategy_.withTransaction(manager) + + const productsWithPrices = await Promise.all( + productArray.map(async (p) => { + if (p.variants?.length) { + p.variants = await Promise.all( + p.variants.map(async (v) => { + const prices = + await priceSelectionStrategy.calculateVariantPrice(v.id, { + region_id: regionId, + currency_code: currencyCode, + cart_id: cart_id, + customer_id: customer_id, + include_discount_prices, + }) + + return { + ...v, + prices: prices.prices, + original_price: prices.originalPrice, + calculated_price: prices.calculatedPrice, + calculated_price_type: prices.calculatedPriceType, + } + }) + ) + } + + return p + }) + ) + + return Array.isArray(products) + ? productsWithPrices + : productsWithPrices[0] + }) + } } export default ProductService diff --git a/packages/medusa/src/strategies/price-selection.ts b/packages/medusa/src/strategies/price-selection.ts index d128169f79..216ff7bc79 100644 --- a/packages/medusa/src/strategies/price-selection.ts +++ b/packages/medusa/src/strategies/price-selection.ts @@ -7,6 +7,7 @@ import { } from "../interfaces/price-selection-strategy" import { MoneyAmountRepository } from "../repositories/money-amount" import { EntityManager } from "typeorm" +import { MedusaError } from "medusa-core-utils" class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { private moneyAmountRepository_: typeof MoneyAmountRepository diff --git a/packages/medusa/src/types/price-selection.ts b/packages/medusa/src/types/price-selection.ts new file mode 100644 index 0000000000..0ba7dd99c2 --- /dev/null +++ b/packages/medusa/src/types/price-selection.ts @@ -0,0 +1,15 @@ +import { IsOptional, IsString } from "class-validator" + +export class PriceSelectionParams { + @IsOptional() + @IsString() + cart_id?: string + + @IsOptional() + @IsString() + region_id?: string + + @IsOptional() + @IsString() + currency_code?: string +} diff --git a/packages/medusa/src/types/product-variant.ts b/packages/medusa/src/types/product-variant.ts index 9578dd48c1..4cf480caf2 100644 --- a/packages/medusa/src/types/product-variant.ts +++ b/packages/medusa/src/types/product-variant.ts @@ -25,6 +25,13 @@ export type ProductVariantPrice = { max_quantity?: number } +export type GetRegionPriceContext = { + regionId: string + quantity?: number + customer_id?: string + include_discount_prices?: boolean +} + export type ProductVariantOption = { option_id: string value: string