From 69e579758f81332094d6f0dfa6fbcbc359b0d92c Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:54:03 +0200 Subject: [PATCH] fix(medusa, medusa-js): Use price selection strategy for `GET /admin/variants` (#2270) **What** - Adds the use of price selection strategy to the endpoint `GET /admin/variants` - Updates medusa-js to reflect this change (expanding the parameters). **Testing** - Adds a new integration test validating that returned variants are now of type PricedVariant, with the expected fields: original_price, calculated_price, etc. **Why** - Our current RMA flows (in our admin dashboard) relied heavily on simply using `order.tax_rate` to calculate variant prices in the different RMA menus. As taxes in Medusa, have become feature complete this approach had become very naive and has several potential issues. Moving the responsibility for calculating the correct prices guarantees that we always show the correct prices to admins. --- .changeset/few-maps-hammer.md | 6 + .../admin/__snapshots__/variant.js.snap | 142 +++++++++++++ .../api/__tests__/admin/variant.js | 119 +++++++++++ .../api/helpers/admin-variants-seeder.js | 192 ++++++++++++++++++ .../api/helpers/store-product-seeder.js | 2 + integration-tests/api/package.json | 6 +- integration-tests/api/yarn.lock | 74 +++---- .../medusa-js/src/resources/admin/variants.ts | 2 +- .../src/api/routes/admin/variants/index.ts | 19 +- .../routes/admin/variants/list-variants.ts | 150 +++++++++++--- .../routes/store/variants/list-variants.ts | 12 +- packages/medusa/src/types/price-selection.ts | 6 + 12 files changed, 647 insertions(+), 83 deletions(-) create mode 100644 .changeset/few-maps-hammer.md create mode 100644 integration-tests/api/__tests__/admin/__snapshots__/variant.js.snap create mode 100644 integration-tests/api/helpers/admin-variants-seeder.js diff --git a/.changeset/few-maps-hammer.md b/.changeset/few-maps-hammer.md new file mode 100644 index 0000000000..eecd0eb0ad --- /dev/null +++ b/.changeset/few-maps-hammer.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/medusa-js": patch +--- + +Adds the use of price selection strategy to retrieving variants in the admin API. This moves the responsibility of tax calculations from the frontend (admin) to the backend. diff --git a/integration-tests/api/__tests__/admin/__snapshots__/variant.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/variant.js.snap new file mode 100644 index 0000000000..6f2d966c25 --- /dev/null +++ b/integration-tests/api/__tests__/admin/__snapshots__/variant.js.snap @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`/admin/products GET /admin/variants price selection strategy selects prices based on the passed currency code 1`] = ` +Object { + "count": 1, + "variants": Array [ + Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": 80, + "calculated_price_incl_tax": null, + "calculated_price_type": "sale", + "calculated_tax": 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": Any, + "origin_country": null, + "original_price": 100, + "original_price_incl_tax": null, + "original_tax": null, + "prices": Any, + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "tax_rates": null, + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, + ], +} +`; + +exports[`/admin/products GET /admin/variants price selection strategy selects prices based on the passed region id 1`] = ` +Object { + "count": 1, + "variants": Array [ + Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": 80, + "calculated_price_incl_tax": 80, + "calculated_price_type": "sale", + "calculated_tax": 0, + "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": Any, + "origin_country": null, + "original_price": 100, + "original_price_incl_tax": 100, + "original_tax": 0, + "prices": Any, + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "tax_rates": Array [ + Object { + "code": "default", + "name": "default", + "rate": 0, + }, + ], + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, + ], +} +`; + +exports[`/admin/products GET /admin/variants price selection strategy selects prices based on the passed region id and customer id 1`] = ` +Object { + "count": 1, + "variants": Array [ + Object { + "allow_backorder": false, + "barcode": "test-barcode", + "calculated_price": 40, + "calculated_price_incl_tax": 40, + "calculated_price_type": "sale", + "calculated_tax": 0, + "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": Any, + "origin_country": null, + "original_price": 100, + "original_price_incl_tax": 100, + "original_tax": 0, + "prices": Any, + "product": Any, + "product_id": "test-product", + "sku": "test-sku", + "tax_rates": Array [ + Object { + "code": "default", + "name": "default", + "rate": 0, + }, + ], + "title": "Test variant", + "upc": "test-upc", + "updated_at": Any, + "weight": null, + "width": null, + }, + ], +} +`; diff --git a/integration-tests/api/__tests__/admin/variant.js b/integration-tests/api/__tests__/admin/variant.js index 3f8934d830..baa0289364 100644 --- a/integration-tests/api/__tests__/admin/variant.js +++ b/integration-tests/api/__tests__/admin/variant.js @@ -3,9 +3,12 @@ const path = require("path") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") +const { simpleProductFactory } = require("../../factories") const adminSeeder = require("../../helpers/admin-seeder") +const adminVariantsSeeder = require("../../helpers/admin-variants-seeder") const productSeeder = require("../../helpers/product-seeder") +const storeProductSeeder = require("../../helpers/store-product-seeder") jest.setTimeout(30000) @@ -117,6 +120,7 @@ describe("/admin/products", () => { it("lists all product variants matching a specific product title", async () => { const api = useApi() + const response = await api .get("/admin/variants?q=Test product1", { headers: { @@ -145,4 +149,119 @@ describe("/admin/products", () => { ) }) }) + + describe("GET /admin/variants price selection strategy", () => { + beforeEach(async () => { + try { + await adminVariantsSeeder(dbConnection) + } catch (err) { + console.log(err) + } + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("selects prices based on the passed currency code", async () => { + const api = useApi() + + const response = await api.get( + "/admin/variants?id=test-variant¤cy_code=usd", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + expect(response.data).toMatchSnapshot({ + variants: [ + { + id: "test-variant", + original_price: 100, + calculated_price: 80, + calculated_price_type: "sale", + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + options: expect.any(Array), + prices: expect.any(Array), + product: expect.any(Object), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }) + }) + + it("selects prices based on the passed region id", async () => { + const api = useApi() + + const response = await api.get( + "/admin/variants?id=test-variant®ion_id=reg-europe", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + expect(response.data).toMatchSnapshot({ + variants: [ + { + id: "test-variant", + original_price: 100, + calculated_price: 80, + calculated_price_type: "sale", + original_price_incl_tax: 100, + calculated_price_incl_tax: 80, + original_tax: 0, + calculated_tax: 0, + options: expect.any(Array), + prices: expect.any(Array), + product: expect.any(Object), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }) + }) + + it("selects prices based on the passed region id and customer id", async () => { + const api = useApi() + + const response = await api.get( + "/admin/variants?id=test-variant®ion_id=reg-europe&customer_id=test-customer", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + + expect(response.data).toMatchSnapshot({ + variants: [ + { + id: "test-variant", + original_price: 100, + calculated_price: 40, + calculated_price_type: "sale", + original_price_incl_tax: 100, + calculated_price_incl_tax: 40, + original_tax: 0, + calculated_tax: 0, + prices: expect.any(Array), + options: expect.any(Array), + product: expect.any(Object), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }) + }) + }) }) diff --git a/integration-tests/api/helpers/admin-variants-seeder.js b/integration-tests/api/helpers/admin-variants-seeder.js new file mode 100644 index 0000000000..9c8a82a63a --- /dev/null +++ b/integration-tests/api/helpers/admin-variants-seeder.js @@ -0,0 +1,192 @@ +const { + Region, + Product, + ProductVariant, + PriceList, + CustomerGroup, + Customer, + Image, + ShippingProfile, + ProductCollection, + ProductOption, +} = 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 tenDaysAgo = ((today) => new Date(today.setDate(today.getDate() - 10)))( + new Date() + ) + + const defaultProfile = await manager.findOne(ShippingProfile, { + type: "default", + }) + + const collection = manager.create(ProductCollection, { + id: "test-collection", + handle: "test-collection", + title: "Test collection", + }) + + await manager.save(collection) + + await manager.insert(Region, { + id: "reg-europe", + name: "Test Region Europe", + currency_code: "eur", + tax_rate: 0, + }) + + await manager.insert(Region, { + id: "reg-us", + name: "Test Region US", + currency_code: "usd", + tax_rate: 0, + }) + + const customer = await manager.create(Customer, { + id: "test-customer", + email: "john@doe.com", + first_name: "John", + last_name: "Doe", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + + const customerGroup = await manager.create(CustomerGroup, { + id: "test-group", + name: "test-group", + }) + + await manager.save(customerGroup) + customer.groups = [customerGroup] + await manager.save(customer) + + const priceListActive = await manager.create(PriceList, { + id: "pl", + name: "VIP sale", + description: "All year sale for VIP customers.", + type: "sale", + status: "active", + }) + + await manager.save(priceListActive) + + const priceListExpired = await manager.create(PriceList, { + id: "pl_expired", + name: "VIP summer sale", + description: "Summer sale for VIP customers.", + type: "sale", + status: "active", + starts_at: tenDaysAgo, + ends_at: yesterday, + }) + + await manager.save(priceListExpired) + + const priceListWithCustomers = await manager.create(PriceList, { + id: "pl_with_customers", + name: "VIP winter sale", + description: "Winter sale for VIP customers.", + type: "sale", + status: "active", + }) + + priceListWithCustomers.customer_groups = [customerGroup] + + await manager.save(priceListWithCustomers) + + const productMultiReg = manager.create(Product, { + id: "test-product", + handle: "test-product-reg", + title: "Multi Reg Test product", + profile_id: defaultProfile.id, + description: "test-product-description", + status: "published", + collection_id: "test-collection", + }) + + const image = manager.create(Image, { + id: "test-image", + url: "test-image.png", + }) + + productMultiReg.images = [image] + + await manager.save(productMultiReg) + + await manager.save(ProductOption, { + id: "test-option", + title: "test-option", + product_id: "test-product", + }) + + const variantMultiReg = await manager.create(ProductVariant, { + id: "test-variant", + inventory_quantity: 10, + title: "Test variant", + variant_rank: 0, + sku: "test-sku", + ean: "test-ean", + upc: "test-upc", + barcode: "test-barcode", + product_id: "test-product", + prices: [ + { + id: "test-price-multi-usd", + currency_code: "usd", + type: "default", + amount: 100, + }, + { + id: "test-price-discount-multi-usd", + currency_code: "usd", + amount: 80, + price_list_id: "pl", + }, + { + id: "test-price-discount-expired-multi-usd", + currency_code: "usd", + amount: 70, + price_list_id: "pl_expired", + }, + { + id: "test-price-multi-eur", + currency_code: "eur", + amount: 100, + }, + { + id: "test-price-discount-multi-eur", + currency_code: "eur", + amount: 80, + price_list_id: "pl", + }, + { + id: "test-price-discount-multi-eur-with-customer", + currency_code: "eur", + amount: 40, + price_list_id: "pl_with_customers", + }, + { + id: "test-price-discount-expired-multi-eur", + currency_code: "eur", + amount: 70, + price_list_id: "pl_expired", + }, + ], + options: [ + { + id: "test-variant-option-reg", + value: "Default variant", + option_id: "test-option", + }, + ], + }) + + await manager.save(variantMultiReg) +} diff --git a/integration-tests/api/helpers/store-product-seeder.js b/integration-tests/api/helpers/store-product-seeder.js index 2290465c1a..a675ec352e 100644 --- a/integration-tests/api/helpers/store-product-seeder.js +++ b/integration-tests/api/helpers/store-product-seeder.js @@ -10,6 +10,8 @@ const { Image, Cart, PriceList, + CustomerGroup, + Customer, } = require("@medusajs/medusa") module.exports = async (connection, data = {}) => { diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index 6580acaf88..b1ad5a6818 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,16 +8,16 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.3.7-dev-1662369149992", + "@medusajs/medusa": "1.4.1-dev-1664548572642", "faker": "^5.5.3", - "medusa-interfaces": "1.3.3-dev-1662369149992", + "medusa-interfaces": "1.3.3-dev-1664548572642", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.19-dev-1662369149992", + "babel-preset-medusa-package": "1.1.19-dev-1664548572642", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index 1ab81d8bb5..5cc0ca5d3c 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1775,9 +1775,9 @@ __metadata: languageName: node linkType: hard -"@medusajs/medusa-cli@npm:1.3.2-dev-1662369149992": - version: 1.3.2-dev-1662369149992 - resolution: "@medusajs/medusa-cli@npm:1.3.2-dev-1662369149992" +"@medusajs/medusa-cli@npm:1.3.3-dev-1664548572642": + version: 1.3.3-dev-1664548572642 + resolution: "@medusajs/medusa-cli@npm:1.3.3-dev-1664548572642" dependencies: "@babel/polyfill": ^7.8.7 "@babel/runtime": ^7.9.6 @@ -1793,8 +1793,8 @@ __metadata: inquirer: ^8.0.0 is-valid-path: ^0.1.1 meant: ^1.0.1 - medusa-core-utils: 1.1.31-dev-1662369149992 - medusa-telemetry: 0.0.13-dev-1662369149992 + medusa-core-utils: 1.1.31-dev-1664548572642 + medusa-telemetry: 0.0.13-dev-1664548572642 netrc-parser: ^3.1.6 open: ^8.0.6 ora: ^5.4.1 @@ -1809,15 +1809,15 @@ __metadata: yargs: ^15.3.1 bin: medusa: cli.js - checksum: b7b163bcbba3ef8d8e4ce35cff8b1e90f764916d88a23eef54475dc616f04997ce423cb402684437dd8ef2594a5f6d5a51a3a30129f18d189b71a857ff429a95 + checksum: 73631f55740e272bf173184df0fe94b8106e6c53a85a06aa2c477227fa19ddf377c9b42e34683a39849e91836d29fd4fbe0192ad2ecc9994c1190994c836c6c1 languageName: node linkType: hard -"@medusajs/medusa@npm:1.3.7-dev-1662369149992": - version: 1.3.7-dev-1662369149992 - resolution: "@medusajs/medusa@npm:1.3.7-dev-1662369149992" +"@medusajs/medusa@npm:1.4.1-dev-1664548572642": + version: 1.4.1-dev-1664548572642 + resolution: "@medusajs/medusa@npm:1.4.1-dev-1664548572642" dependencies: - "@medusajs/medusa-cli": 1.3.2-dev-1662369149992 + "@medusajs/medusa-cli": 1.3.3-dev-1664548572642 "@types/ioredis": ^4.28.10 "@types/lodash": ^4.14.168 awilix: ^4.2.3 @@ -1839,8 +1839,8 @@ __metadata: ioredis-mock: ^5.6.0 iso8601-duration: ^1.3.0 jsonwebtoken: ^8.5.1 - medusa-core-utils: 1.1.31-dev-1662369149992 - medusa-test-utils: 1.1.37-dev-1662369149992 + medusa-core-utils: 1.1.31-dev-1664548572642 + medusa-test-utils: 1.1.37-dev-1664548572642 morgan: ^1.9.1 multer: ^1.4.2 node-schedule: ^2.1.0 @@ -1865,7 +1865,7 @@ __metadata: typeorm: 0.2.x bin: medusa: cli.js - checksum: 8335278bd5019c94919ca73df4b29f8c47daab24fca0a5d4671eda5c2bbddb45a5dd31986f4a3a865e418c2e1c164680a046645a5fc33f49d2132c4a14b42aac + checksum: bd67281e7e7c45913074f45572731f9779d1ed1b999113ea67f6b4ea9216f3ea37df75b66d6e27d2bed1837434370efb3617af24da93571133003ae07b7d2f5e languageName: node linkType: hard @@ -2446,11 +2446,11 @@ __metadata: "@babel/cli": ^7.12.10 "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 - "@medusajs/medusa": 1.3.7-dev-1662369149992 - babel-preset-medusa-package: 1.1.19-dev-1662369149992 + "@medusajs/medusa": 1.4.1-dev-1664548572642 + babel-preset-medusa-package: 1.1.19-dev-1664548572642 faker: ^5.5.3 jest: ^26.6.3 - medusa-interfaces: 1.3.3-dev-1662369149992 + medusa-interfaces: 1.3.3-dev-1664548572642 typeorm: ^0.2.31 languageName: unknown linkType: soft @@ -2757,9 +2757,9 @@ __metadata: languageName: node linkType: hard -"babel-preset-medusa-package@npm:1.1.19-dev-1662369149992": - version: 1.1.19-dev-1662369149992 - resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1662369149992" +"babel-preset-medusa-package@npm:1.1.19-dev-1664548572642": + version: 1.1.19-dev-1664548572642 + resolution: "babel-preset-medusa-package@npm:1.1.19-dev-1664548572642" dependencies: "@babel/plugin-proposal-class-properties": ^7.12.1 "@babel/plugin-proposal-decorators": ^7.12.1 @@ -2773,7 +2773,7 @@ __metadata: core-js: ^3.7.0 peerDependencies: "@babel/core": ^7.11.6 - checksum: 934e46bb0b231cc328eeccf40c9caa47ab48bc021af3355836f4b142690540ae24935b3d45154ac5665517d831b882455cab134733027000900ddd7cef4f2eb1 + checksum: 74f61921185e75fb0c80777208809f7b7e469108b66aefdcb8ba14e4419ac1582d5703c4408488fdbc5282e6bc7740491cc3f2830f964821ff59319f65de7d3a languageName: node linkType: hard @@ -6906,29 +6906,29 @@ __metadata: languageName: node linkType: hard -"medusa-core-utils@npm:1.1.31-dev-1662369149992": - version: 1.1.31-dev-1662369149992 - resolution: "medusa-core-utils@npm:1.1.31-dev-1662369149992" +"medusa-core-utils@npm:1.1.31-dev-1664548572642": + version: 1.1.31-dev-1664548572642 + resolution: "medusa-core-utils@npm:1.1.31-dev-1664548572642" dependencies: joi: ^17.3.0 joi-objectid: ^3.0.1 - checksum: 1a0574f3b4833b1be47cb6ff947c1547ab610eaedaa15710eac2929f5ce6d80948bd92f7a594e199dc85f1d9a6d4ce8c58350ae155632d83991196c13cdbc9cf + checksum: f5f39d7eeffbf8c893d64f72d04e7a3f844718c4b9759094fbf213406e7fb12dc5ec6825a3ceec1d8c3bf462a5e3049ad0d6ddb93a7c7b530cd384b176e3bf8e languageName: node linkType: hard -"medusa-interfaces@npm:1.3.3-dev-1662369149992": - version: 1.3.3-dev-1662369149992 - resolution: "medusa-interfaces@npm:1.3.3-dev-1662369149992" +"medusa-interfaces@npm:1.3.3-dev-1664548572642": + version: 1.3.3-dev-1664548572642 + resolution: "medusa-interfaces@npm:1.3.3-dev-1664548572642" peerDependencies: medusa-core-utils: ^1.1.31 typeorm: 0.x - checksum: eaddeda76f24bc9c2469973eeb0e5bcbfbafa2a0f10505bf79d8e0f6cf12e14ffd7300f03035194457c768f15e16988365e9fccbb4ded170e825253984b3cf0e + checksum: b358ce3d19b48f539569f5c69e60cb9927ac59bf2fabb9f24dab1d7ae8fa3a42fd5c4b127f37c119139b0063ee071e2b370d61749c5971a32af32f130713e700 languageName: node linkType: hard -"medusa-telemetry@npm:0.0.13-dev-1662369149992": - version: 0.0.13-dev-1662369149992 - resolution: "medusa-telemetry@npm:0.0.13-dev-1662369149992" +"medusa-telemetry@npm:0.0.13-dev-1664548572642": + version: 0.0.13-dev-1664548572642 + resolution: "medusa-telemetry@npm:0.0.13-dev-1664548572642" dependencies: axios: ^0.21.1 axios-retry: ^3.1.9 @@ -6939,18 +6939,18 @@ __metadata: is-docker: ^2.2.1 remove-trailing-slash: ^0.1.1 uuid: ^8.3.2 - checksum: 76a4c1e417b05baa5f8d0bacfa78cb50f3fece1490722d08d751b7269ae715a6b6b668dbc9d580b3e43e484d13d57e03fbbfe7a696eab355fb39b0a46f87f5ec + checksum: 5be02967eb94e7db2883b6c22c1e213979d04bcd63a59c38ddc6f5711b97bc5fd7fd9e59833c6ecf56c936ab8847d7860bd429498670450ab48d7889d12d7919 languageName: node linkType: hard -"medusa-test-utils@npm:1.1.37-dev-1662369149992": - version: 1.1.37-dev-1662369149992 - resolution: "medusa-test-utils@npm:1.1.37-dev-1662369149992" +"medusa-test-utils@npm:1.1.37-dev-1664548572642": + version: 1.1.37-dev-1664548572642 + resolution: "medusa-test-utils@npm:1.1.37-dev-1664548572642" dependencies: "@babel/plugin-transform-classes": ^7.9.5 - medusa-core-utils: 1.1.31-dev-1662369149992 + medusa-core-utils: 1.1.31-dev-1664548572642 randomatic: ^3.1.1 - checksum: f8dab2548bdae681141fce55be9d1a770d62a237a682fc0df02ce6d03bf27e908d82058f1e1d7ced1aa5a9a66c28381ec9c8b63c75100921b128b9808594e33d + checksum: c91853a098ec381c8d7768f8f450ea0b94f6b9a6f44bae87fa0820574c4adb9d1b6a628d32e901a6b041a5690ddaa93235a4875d526e3a68e3aee7ef434012d6 languageName: node linkType: hard diff --git a/packages/medusa-js/src/resources/admin/variants.ts b/packages/medusa-js/src/resources/admin/variants.ts index df504420d8..5a6e153cd6 100644 --- a/packages/medusa-js/src/resources/admin/variants.ts +++ b/packages/medusa-js/src/resources/admin/variants.ts @@ -1,4 +1,4 @@ -import { AdminVariantsListRes, AdminGetVariantsParams } from "@medusajs/medusa" +import { AdminGetVariantsParams, AdminVariantsListRes } from "@medusajs/medusa" import qs from "qs" import { ResponsePromise } from "../.." import BaseResource from "../base" diff --git a/packages/medusa/src/api/routes/admin/variants/index.ts b/packages/medusa/src/api/routes/admin/variants/index.ts index 53df40b771..3e5cf1a7f2 100644 --- a/packages/medusa/src/api/routes/admin/variants/index.ts +++ b/packages/medusa/src/api/routes/admin/variants/index.ts @@ -1,15 +1,26 @@ import { Router } from "express" -import { PaginatedResponse } from "../../../../types/common" import { ProductVariant } from "../../../../models/product-variant" -import middlewares from "../../../middlewares" +import { PaginatedResponse } from "../../../../types/common" +import { PricedVariant } from "../../../../types/pricing" +import middlewares, { transformQuery } from "../../../middlewares" +import { AdminGetVariantsParams } from "./list-variants" const route = Router() export default (app) => { app.use("/variants", route) - route.get("/", middlewares.wrap(require("./list-variants").default)) + route.get( + "/", + transformQuery(AdminGetVariantsParams, { + defaultRelations: defaultAdminVariantRelations, + defaultFields: defaultAdminVariantFields, + allowedFields: allowedAdminVariantFields, + isList: true, + }), + middlewares.wrap(require("./list-variants").default) + ) return app } @@ -69,7 +80,7 @@ export const allowedAdminVariantRelations: (keyof ProductVariant)[] = [ ] export type AdminVariantsListRes = PaginatedResponse & { - variants: ProductVariant[] + variants: PricedVariant[] } export * from "./list-variants" diff --git a/packages/medusa/src/api/routes/admin/variants/list-variants.ts b/packages/medusa/src/api/routes/admin/variants/list-variants.ts index dcbd18d98b..0cd323843e 100644 --- a/packages/medusa/src/api/routes/admin/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/admin/variants/list-variants.ts @@ -1,12 +1,16 @@ import { IsInt, IsOptional, IsString } from "class-validator" -import { defaultAdminVariantFields, defaultAdminVariantRelations } from "./" -import { FilterableProductVariantProps } from "../../../../types/product-variant" -import { FindConfig } from "../../../../types/common" -import { ProductVariant } from "../../../../models/product-variant" -import ProductVariantService from "../../../../services/product-variant" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" +import { omit } from "lodash" +import { + CartService, + PricingService, + RegionService, +} from "../../../../services" +import ProductVariantService from "../../../../services/product-variant" +import { NumericalComparisonOperator } from "../../../../types/common" +import { AdminPriceSelectionParams } from "../../../../types/price-selection" +import { IsType } from "../../../../utils/validators/is-type" /** * @oas [get] /variants @@ -15,9 +19,51 @@ import { validator } from "../../../../utils/validator" * description: "Retrieves a list of Product Variants" * x-authenticated: true * parameters: - * - (query) q {string} Query used for searching variants. - * - (query) offset=0 {integer} How many variants to skip in the result. - * - (query) limit=20 {integer} Limit the number of variants returned. + * - (query) id {string} A Product Variant id to filter by. + * - (query) ids {string} A comma separated list of Product Variant ids to filter by. + * - (query) expand {string} A comma separated list of Product Variant relations to load. + * - (query) fields {string} A comma separated list of Product Variant fields to include. + * - (query) offset=0 {number} How many product variants to skip in the result. + * - (query) limit=100 {number} Maximum number of Product Variants to return. + * - (query) cart_id {string} The id of the cart to use for price selection. + * - (query) region_id {string} The id of the region to use for price selection. + * - (query) currency_code {string} The currency code to use for price selection. + * - (query) customer_id {string} The id of the customer to use for price selection. + * - in: query + * name: title + * style: form + * explode: false + * description: product variant title to search for. + * schema: + * oneOf: + * - type: string + * description: a single title to search by + * - type: array + * description: multiple titles to search by + * items: + * type: string + * - in: query + * name: inventory_quantity + * description: Filter by available inventory quantity + * schema: + * oneOf: + * - type: number + * description: a specific number to search by. + * - type: object + * description: search using less and greater than comparisons. + * properties: + * lt: + * type: number + * description: filter by inventory quantity less than this number + * gt: + * type: number + * description: filter by inventory quantity greater than this number + * lte: + * type: number + * description: filter by inventory quantity less than or equal to this number + * gte: + * type: number + * description: filter by inventory quantity greater than or equal to this number * x-codeSamples: * - lang: JavaScript * label: JS Client @@ -77,44 +123,84 @@ export default async (req, res) => { "productVariantService" ) - const { offset, limit, q } = await validator( - AdminGetVariantsParams, - req.query + const pricingService: PricingService = req.scope.resolve("pricingService") + const cartService: CartService = req.scope.resolve("cartService") + const regionService: RegionService = req.scope.resolve("regionService") + + // We need to remove the price selection params from the array of fields + const cleanFilterableFields = omit(req.filterableFields, [ + "cart_id", + "region_id", + "currency_code", + "customer_id", + ]) + + const [rawVariants, count] = await variantService.listAndCount( + cleanFilterableFields, + req.listConfig ) - const selector: FilterableProductVariantProps = {} - - if ("q" in req.query) { - selector.q = q + let regionId = req.validatedQuery.region_id + let currencyCode = req.validatedQuery.currency_code + if (req.validatedQuery.cart_id) { + const cart = await cartService.retrieve(req.validatedQuery.cart_id, { + select: ["id", "region_id"], + }) + const region = await regionService.retrieve(cart.region_id, { + select: ["id", "currency_code"], + }) + regionId = region.id + currencyCode = region.currency_code } - const listConfig: FindConfig = { - select: defaultAdminVariantFields, - relations: defaultAdminVariantRelations, - skip: offset, - take: limit, - } + const variants = await pricingService.setVariantPrices(rawVariants, { + cart_id: req.validatedQuery.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: req.validatedQuery.customer_id, + include_discount_prices: true, + }) - const [variants, count] = await variantService.listAndCount( - selector, - listConfig - ) - - res.json({ variants, count, offset, limit }) + res.json({ + variants, + count, + offset: req.listConfig.offset, + limit: req.listConfig.limit, + }) } -export class AdminGetVariantsParams { - @IsString() +export class AdminGetVariantsParams extends AdminPriceSelectionParams { @IsOptional() + @IsString() q?: string - @IsInt() @IsOptional() + @IsInt() @Type(() => Number) limit?: number = 20 - @IsInt() @IsOptional() + @IsInt() @Type(() => Number) offset?: number = 0 + + @IsOptional() + @IsString() + expand?: string + + @IsString() + @IsOptional() + fields?: string + + @IsOptional() + @IsType([String, [String]]) + id?: string | string[] + + @IsOptional() + @IsType([String, [String]]) + title?: string | string[] + + @IsOptional() + @IsType([Number, NumericalComparisonOperator]) + inventory_quantity?: number | NumericalComparisonOperator } 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 614569ff52..389ed81d91 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/store/variants/list-variants.ts @@ -1,19 +1,19 @@ +import { IsInt, IsOptional, IsString } from "class-validator" import { CartService, PricingService, ProductVariantService, RegionService, } from "../../../../services" -import { IsInt, IsOptional, IsString } from "class-validator" -import { FilterableProductVariantProps } from "../../../../types/product-variant" -import { IsType } from "../../../../utils/validators/is-type" +import { Type } from "class-transformer" +import { omit } from "lodash" +import { defaultStoreVariantRelations } from "." import { NumericalComparisonOperator } from "../../../../types/common" import { PriceSelectionParams } from "../../../../types/price-selection" -import { Type } from "class-transformer" -import { defaultStoreVariantRelations } from "." -import { omit } from "lodash" +import { FilterableProductVariantProps } from "../../../../types/product-variant" import { validator } from "../../../../utils/validator" +import { IsType } from "../../../../utils/validators/is-type" /** * @oas [get] /variants diff --git a/packages/medusa/src/types/price-selection.ts b/packages/medusa/src/types/price-selection.ts index 0ba7dd99c2..53c8c4a159 100644 --- a/packages/medusa/src/types/price-selection.ts +++ b/packages/medusa/src/types/price-selection.ts @@ -13,3 +13,9 @@ export class PriceSelectionParams { @IsString() currency_code?: string } + +export class AdminPriceSelectionParams extends PriceSelectionParams { + @IsOptional() + @IsString() + customer_id?: string +}