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 +}