From 7c46b0f88b65467a9fb734061a7cc8e7bdb3c85c Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 11 Mar 2024 18:00:16 +0100 Subject: [PATCH] feat(medusa): added list price list products endpoint (#6617) what: - adds an endpoint to list price list products --- .changeset/gorgeous-chairs-fetch.md | 5 + .../admin/list-price-list-products.spec.ts | 255 ------------------ .../price-lists/admin/price-lists.spec.ts | 153 ++++++----- .../__tests__/product/admin/products.spec.ts | 200 ++++++++++++++ .../api-v2/admin/price-lists/[id]/route.ts | 6 +- .../api-v2/admin/price-lists/middlewares.ts | 6 + .../price-lists/{utils => queries}/index.ts | 0 .../price-lists/queries/list-price-lists.ts | 90 +++++++ .../api-v2/admin/price-lists/query-config.ts | 84 +++--- .../src/api-v2/admin/price-lists/route.ts | 6 +- .../src/api-v2/admin/price-lists/types.ts | 22 ++ .../price-lists/utils/list-price-lists.ts | 123 --------- .../src/api-v2/admin/products/middlewares.ts | 9 +- .../medusa/src/api-v2/admin/products/route.ts | 51 +++- .../src/api-v2/admin/products/validators.ts | 20 +- 15 files changed, 514 insertions(+), 516 deletions(-) create mode 100644 .changeset/gorgeous-chairs-fetch.md delete mode 100644 integration-tests/modules/__tests__/price-lists/admin/list-price-list-products.spec.ts create mode 100644 integration-tests/modules/__tests__/product/admin/products.spec.ts rename packages/medusa/src/api-v2/admin/price-lists/{utils => queries}/index.ts (100%) create mode 100644 packages/medusa/src/api-v2/admin/price-lists/queries/list-price-lists.ts create mode 100644 packages/medusa/src/api-v2/admin/price-lists/types.ts delete mode 100644 packages/medusa/src/api-v2/admin/price-lists/utils/list-price-lists.ts diff --git a/.changeset/gorgeous-chairs-fetch.md b/.changeset/gorgeous-chairs-fetch.md new file mode 100644 index 0000000000..a314f9dcc2 --- /dev/null +++ b/.changeset/gorgeous-chairs-fetch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): added list price list products endpoint diff --git a/integration-tests/modules/__tests__/price-lists/admin/list-price-list-products.spec.ts b/integration-tests/modules/__tests__/price-lists/admin/list-price-list-products.spec.ts deleted file mode 100644 index 6826bda6fe..0000000000 --- a/integration-tests/modules/__tests__/price-lists/admin/list-price-list-products.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { simpleProductFactory } from "../../../../factories" - -import { - IPricingModuleService, - PriceListStatus, - PriceListType, -} from "@medusajs/types" -import adminSeeder from "../../../../helpers/admin-seeder" -import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -const adminHeaders = { - headers: { - "x-medusa-access-token": "test_token", - }, -} - -const env = { - MEDUSA_FF_MEDUSA_V2: true, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe.skip("GET /admin/price-lists/:id/products", () => { - let appContainer - let product - let product2 - let variant - let pricingModuleService: IPricingModuleService - - beforeAll(async () => { - appContainer = getContainer() - pricingModuleService = appContainer.resolve("pricingModuleService") - }) - - beforeEach(async () => { - await adminSeeder(dbConnection) - - product = await simpleProductFactory(dbConnection, { - id: "test-product-with-variant", - title: "uniquely fun product", - variants: [ - { - options: [{ option_id: "test-product-option-1", value: "test" }], - }, - ], - options: [ - { - id: "test-product-option-1", - title: "Test option 1", - }, - ], - }) - - variant = product.variants[0] - - product2 = await simpleProductFactory(dbConnection, { - id: "test-product-with-variant-2", - title: "uniquely fun product 2", - variants: [ - { - options: [ - { option_id: "test-product-option-2", value: "test 2" }, - ], - }, - ], - options: [ - { - id: "test-product-option-2", - title: "Test option 2", - }, - ], - }) - }) - - it("should list all products in a price list", async () => { - const priceSet = await createVariantPriceSet({ - container: appContainer, - variantId: variant.id, - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - rules: [], - }) - - const [priceList] = await pricingModuleService.createPriceLists([ - { - title: "test price list", - description: "test", - ends_at: new Date(), - starts_at: new Date(), - status: PriceListStatus.ACTIVE, - type: PriceListType.OVERRIDE, - prices: [ - { - amount: 5000, - currency_code: "usd", - price_set_id: priceSet.id, - }, - ], - }, - ]) - - let response = await api.get( - `/admin/price-lists/${priceList.id}/products`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: expect.any(String), - title: expect.any(String), - handle: expect.any(String), - subtitle: null, - description: null, - is_giftcard: false, - status: "draft", - thumbnail: null, - weight: null, - length: null, - height: null, - width: null, - origin_country: null, - hs_code: null, - mid_code: null, - material: null, - collection_id: null, - collection: null, - type_id: null, - type: null, - discountable: true, - external_id: null, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - metadata: null, - }), - ]) - - response = await api.get( - `/admin/products?price_list_id[]=${priceList.id}`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: expect.any(String), - title: expect.any(String), - handle: expect.any(String), - subtitle: null, - description: null, - is_giftcard: false, - status: "draft", - thumbnail: null, - weight: null, - length: null, - height: null, - width: null, - origin_country: null, - hs_code: null, - mid_code: null, - material: null, - collection_id: null, - collection: null, - type_id: null, - type: null, - discountable: true, - external_id: null, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - metadata: null, - }), - ]) - }) - - it("should list all products constrained by search query in a price list", async () => { - const priceSet = await createVariantPriceSet({ - container: appContainer, - variantId: variant.id, - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - rules: [], - }) - - const [priceList] = await pricingModuleService.createPriceLists([ - { - title: "test price list", - description: "test", - ends_at: new Date(), - starts_at: new Date(), - status: PriceListStatus.ACTIVE, - type: PriceListType.OVERRIDE, - prices: [ - { - amount: 5000, - currency_code: "usd", - price_set_id: priceSet.id, - }, - ], - }, - ]) - - let response = await api.get( - `/admin/price-lists/${priceList.id}/products?q=shouldnotreturnanything`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(0) - expect(response.data.products).toEqual([]) - - response = await api.get( - `/admin/price-lists/${priceList.id}/products?q=uniquely`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: expect.any(String), - }), - ]) - - response = await api.get( - `/admin/price-lists/${priceList.id}/products?q=`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.products).toEqual([ - expect.objectContaining({ - id: expect.any(String), - }), - ]) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts b/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts index a74413275d..fb73eb1aa8 100644 --- a/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts +++ b/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts @@ -8,7 +8,7 @@ import { PriceListType, } from "@medusajs/types" import { medusaIntegrationTestRunner } from "medusa-test-utils" -import adminSeeder from "../../../../helpers/admin-seeder" +import { createAdminUser } from "../../../../helpers/create-admin-user" import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" jest.setTimeout(50000) @@ -24,7 +24,9 @@ medusaIntegrationTestRunner({ describe("Admin: Price Lists API", () => { let appContainer let product + let product2 let variant + let variant2 let region let customerGroup let pricingModule: IPricingModuleService @@ -41,7 +43,8 @@ medusaIntegrationTestRunner({ }) beforeEach(async () => { - await adminSeeder(dbConnection) + await createAdminUser(dbConnection, adminHeaders, appContainer) + customerGroup = await customerModule.createCustomerGroup({ name: "VIP", }) @@ -67,16 +70,11 @@ medusaIntegrationTestRunner({ }) describe("GET /admin/price-lists", () => { - it("should get price list and its money amounts with variants", async () => { + it("should get all price lists and its prices with rules", async () => { const priceSet = await createVariantPriceSet({ container: appContainer, variantId: variant.id, - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], + prices: [], }) await pricingModule.createPriceLists([ @@ -92,6 +90,9 @@ medusaIntegrationTestRunner({ amount: 5000, currency_code: "usd", price_set_id: priceSet.id, + rules: { + region_id: region.id, + }, }, ], rules: { @@ -100,8 +101,42 @@ medusaIntegrationTestRunner({ }, ]) - let response = await api.get( - `/admin/price-lists?fields=id,created_at,customer_groups.id,customer_groups.name,prices.id,prices.currency_code,prices.amount,prices.min_quantity,prices.max_quantity,prices.region_id,prices.variant_id`, + let response = await api.get(`/admin/price-lists`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.price_lists).toEqual([ + { + id: expect.any(String), + type: "override", + description: "test", + title: "test price list", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + rules: { + customer_group_id: [customerGroup.id], + }, + prices: [ + { + id: expect.any(String), + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + variant_id: variant.id, + rules: { + region_id: region.id, + }, + }, + ], + }, + ]) + + response = await api.get( + `/admin/price-lists?fields=id,created_at,rules,prices.rules,prices.amount`, adminHeaders ) @@ -111,59 +146,28 @@ medusaIntegrationTestRunner({ { id: expect.any(String), created_at: expect.any(String), + rules: { + customer_group_id: [customerGroup.id], + }, prices: [ { - id: expect.any(String), - currency_code: "usd", amount: 5000, - min_quantity: null, - max_quantity: null, - variant_id: expect.any(String), - region_id: null, + rules: { + region_id: region.id, + }, }, ], - customer_groups: [ - { - id: expect.any(String), - name: "VIP", - }, - ], - }, - ]) - - response = await api.get(`/admin/price-lists`, adminHeaders) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.price_lists).toEqual([ - { - id: expect.any(String), - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - name: "test price list", - description: "test", - type: "override", - status: "active", - starts_at: expect.any(String), - ends_at: expect.any(String), }, ]) }) }) describe("GET /admin/price-lists/:id", () => { - it("should get price list and its money amounts with variants", async () => { + it("should retrieve a price list and its prices with rules", async () => { const priceSet = await createVariantPriceSet({ container: appContainer, variantId: variant.id, - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - rules: [], + prices: [], }) const [priceList] = await pricingModule.createPriceLists([ @@ -179,26 +183,14 @@ medusaIntegrationTestRunner({ amount: 5000, currency_code: "usd", price_set_id: priceSet.id, + rules: { + region_id: region.id, + }, }, ], - }, - ]) - - await pricingModule.createPriceLists([ - { - title: "test price list 1", - description: "test 1", - ends_at: new Date(), - starts_at: new Date(), - status: PriceListStatus.ACTIVE, - type: PriceListType.OVERRIDE, - prices: [ - { - amount: 5000, - currency_code: "usd", - price_set_id: priceSet.id, - }, - ], + rules: { + customer_group_id: [customerGroup.id], + }, }, ]) @@ -211,15 +203,30 @@ medusaIntegrationTestRunner({ expect(response.data.price_list).toEqual( expect.objectContaining({ id: expect.any(String), - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - name: "test price list", - description: "test", type: "override", + description: "test", + title: "test price list", status: "active", starts_at: expect.any(String), ends_at: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + rules: { + customer_group_id: [customerGroup.id], + }, + prices: [ + { + id: expect.any(String), + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + variant_id: variant.id, + rules: { + region_id: region.id, + }, + }, + ], }) ) diff --git a/integration-tests/modules/__tests__/product/admin/products.spec.ts b/integration-tests/modules/__tests__/product/admin/products.spec.ts new file mode 100644 index 0000000000..a88df75422 --- /dev/null +++ b/integration-tests/modules/__tests__/product/admin/products.spec.ts @@ -0,0 +1,200 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreateProductDTO, + IPricingModuleService, + IProductModuleService, + PriceListStatus, + PriceListType, + ProductDTO, + ProductVariantDTO, +} from "@medusajs/types" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../../helpers/create-admin-user" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } + +async function createProductsWithVariants( + productModule: IProductModuleService, + productsData: CreateProductDTO +): Promise<[ProductDTO, ProductVariantDTO[]]> { + const { variants: variantsData, ...productData } = productsData + + const [product] = await productModule.create([productData]) + + const variantsDataWithProductId = variantsData?.map((variantData) => { + return { ...variantData, product_id: product.id } + }) + + const variants = variantsDataWithProductId + ? await productModule.createVariants(variantsDataWithProductId) + : [] + + return [product, variants] +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Admin: Products API", () => { + let appContainer + let product + let product2 + let product3 + let variant + let variant2 + let variant3 + let pricingModule: IPricingModuleService + let productModule: IProductModuleService + + beforeAll(async () => { + appContainer = getContainer() + pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING) + productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT) + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, appContainer) + }) + + describe("GET /admin/products", () => { + describe("should filter products by price lists", () => { + beforeEach(async () => { + ;[product, [variant]] = await createProductsWithVariants( + productModule, + { + title: "test product 1", + variants: [{ title: "test variant 1" }], + } + ) + ;[product2, [variant2]] = await createProductsWithVariants( + productModule, + { + title: "test product 2 uniquely", + variants: [{ title: "test variant 2" }], + } + ) + ;[product3, [variant3]] = await createProductsWithVariants( + productModule, + { + title: "product not in price list", + variants: [{ title: "test variant 3" }], + } + ) + }) + + it("should list all products in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [], + rules: [], + }) + + const priceSet2 = await createVariantPriceSet({ + container: appContainer, + variantId: variant2.id, + prices: [], + rules: [], + }) + + const [priceList] = await pricingModule.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + { + amount: 6000, + currency_code: "usd", + price_set_id: priceSet2.id, + }, + ], + }, + ]) + + let response = await api.get( + `/admin/products?price_list_id[]=${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + title: product.title, + }), + expect.objectContaining({ + id: product2.id, + title: product2.title, + }), + ]) + ) + }) + + it("should list all products constrained by search query in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant2.id, + prices: [], + rules: [], + }) + + const [priceList] = await pricingModule.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + let response = await api.get( + `/admin/products?price_list_id[]=${priceList.id}&q=shouldnotreturnanything`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(0) + expect(response.data.products).toEqual([]) + + response = await api.get( + `/admin/products?price_list_id[]=${priceList.id}&q=uniquely`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: product2.id, + }), + ]) + }) + }) + }) + }) + }, +}) diff --git a/packages/medusa/src/api-v2/admin/price-lists/[id]/route.ts b/packages/medusa/src/api-v2/admin/price-lists/[id]/route.ts index 8d5998db08..0392457639 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/[id]/route.ts @@ -3,7 +3,8 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../types/routing" -import { listPriceLists } from "../utils" +import { listPriceLists } from "../queries" +import { adminPriceListRemoteQueryFields } from "../query-config" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -12,7 +13,8 @@ export const GET = async ( const id = req.params.id const [[priceList], count] = await listPriceLists({ container: req.scope, - fields: req.retrieveConfig.select!, + remoteQueryFields: adminPriceListRemoteQueryFields, + apiFields: req.retrieveConfig.select!, variables: { filters: { id }, skip: 0, diff --git a/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts index f93250a61d..6895af8a9b 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts @@ -1,5 +1,6 @@ import { transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" import { AdminGetPriceListsParams, @@ -7,6 +8,11 @@ import { } from "./validators" export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["ALL"], + matcher: "/admin/price-lists*", + middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], + }, { method: ["GET"], matcher: "/admin/price-lists", diff --git a/packages/medusa/src/api-v2/admin/price-lists/utils/index.ts b/packages/medusa/src/api-v2/admin/price-lists/queries/index.ts similarity index 100% rename from packages/medusa/src/api-v2/admin/price-lists/utils/index.ts rename to packages/medusa/src/api-v2/admin/price-lists/queries/index.ts diff --git a/packages/medusa/src/api-v2/admin/price-lists/queries/list-price-lists.ts b/packages/medusa/src/api-v2/admin/price-lists/queries/list-price-lists.ts new file mode 100644 index 0000000000..6e23f486b6 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/queries/list-price-lists.ts @@ -0,0 +1,90 @@ +import { + MedusaContainer, + PriceListRuleDTO, + PriceSetMoneyAmountDTO, + ProductVariantDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { cleanResponseData } from "../../../../utils/clean-response-data" +import { AdminPriceListRemoteQueryDTO } from "../types" + +export async function listPriceLists({ + container, + remoteQueryFields, + apiFields, + variables, +}: { + container: MedusaContainer + remoteQueryFields: string[] + apiFields: string[] + variables: Record +}): Promise<[AdminPriceListRemoteQueryDTO[], number]> { + const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const queryObject = remoteQueryObjectFromString({ + entryPoint: "price_list", + fields: remoteQueryFields, + variables, + }) + + const { rows: priceLists, metadata } = await remoteQuery(queryObject) + + if (!metadata.count) { + return [[], 0] + } + + for (const priceList of priceLists) { + priceList.rules = buildPriceListRules(priceList.price_list_rules || []) + priceList.prices = buildPriceSetPrices( + priceList.price_set_money_amounts || [] + ) + } + + const sanitizedPriceLists: AdminPriceListRemoteQueryDTO[] = priceLists.map( + (priceList) => cleanResponseData(priceList, apiFields) + ) + + return [sanitizedPriceLists, metadata.count] +} + +function buildPriceListRules( + priceListRules: PriceListRuleDTO[] +): Record { + return priceListRules.reduce((acc, curr) => { + const ruleAttribute = curr.rule_type.rule_attribute + const ruleValues = curr.price_list_rule_values || [] + + if (ruleAttribute) { + acc[ruleAttribute] = ruleValues.map((ruleValue) => ruleValue.value) + } + + return acc + }, {}) +} + +function buildPriceSetPrices( + priceSetMoneyAmounts: (PriceSetMoneyAmountDTO & { + price_set: PriceSetMoneyAmountDTO["price_set"] & { + variant?: ProductVariantDTO + } + })[] +): Record[] { + return priceSetMoneyAmounts.map((priceSetMoneyAmount) => { + const productVariant = priceSetMoneyAmount.price_set?.variant + const rules = priceSetMoneyAmount.price_rules?.reduce((acc, curr) => { + if (curr.rule_type.rule_attribute) { + acc[curr.rule_type.rule_attribute] = curr.value + } + + return acc + }, {}) + + return { + ...priceSetMoneyAmount.money_amount, + variant_id: productVariant?.id ?? null, + rules, + } + }) +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/query-config.ts b/packages/medusa/src/api-v2/admin/price-lists/query-config.ts index 7554e40754..c08ef68afb 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/query-config.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/query-config.ts @@ -1,53 +1,57 @@ export enum PriceListRelations { - CUSTOMER_GROUPS = "customer_groups", PRICES = "prices", } -export const priceListRemoteQueryFields = { - fields: [ - "id", - "type", - "description", - "title", - "status", - "starts_at", - "ends_at", - "created_at", - "updated_at", - "deleted_at", - ], - pricesFields: [ - "price_set_money_amounts.money_amount.id", - "price_set_money_amounts.money_amount.currency_code", - "price_set_money_amounts.money_amount.amount", - "price_set_money_amounts.money_amount.min_quantity", - "price_set_money_amounts.money_amount.max_quantity", - "price_set_money_amounts.money_amount.created_at", - "price_set_money_amounts.money_amount.deleted_at", - "price_set_money_amounts.money_amount.updated_at", - "price_set_money_amounts.price_set.variant.id", - "price_set_money_amounts.price_rules.value", - "price_set_money_amounts.price_rules.rule_type.rule_attribute", - ], - customerGroupsFields: [ - "price_list_rules.price_list_rule_values.value", - "price_list_rules.rule_type.rule_attribute", - "price_set_money_amounts.price_rules.value", - "price_set_money_amounts.price_rules.rule_type.rule_attribute", - ], -} +export const adminPriceListRemoteQueryFields = [ + "id", + "type", + "description", + "title", + "status", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", + "price_set_money_amounts.money_amount.id", + "price_set_money_amounts.money_amount.currency_code", + "price_set_money_amounts.money_amount.amount", + "price_set_money_amounts.money_amount.min_quantity", + "price_set_money_amounts.money_amount.max_quantity", + "price_set_money_amounts.money_amount.created_at", + "price_set_money_amounts.money_amount.deleted_at", + "price_set_money_amounts.money_amount.updated_at", + "price_set_money_amounts.price_set.variant.id", + "price_set_money_amounts.price_rules.value", + "price_set_money_amounts.price_rules.rule_type.rule_attribute", + "price_list_rules.price_list_rule_values.value", + "price_list_rules.rule_type.rule_attribute", +] export const defaultAdminPriceListFields = [ - ...priceListRemoteQueryFields.fields, - "name", + "id", + "type", + "description", + "title", + "status", + "starts_at", + "ends_at", + "rules", + "created_at", + "updated_at", + "prices.amount", + "prices.id", + "prices.currency_code", + "prices.amount", + "prices.min_quantity", + "prices.max_quantity", + "prices.variant_id", + "prices.rules", ] export const defaultAdminPriceListRelations = [] -export const allowedAdminPriceListRelations = [ - PriceListRelations.CUSTOMER_GROUPS, - PriceListRelations.PRICES, -] +export const allowedAdminPriceListRelations = [PriceListRelations.PRICES] export const adminListTransformQueryConfig = { defaultLimit: 50, diff --git a/packages/medusa/src/api-v2/admin/price-lists/route.ts b/packages/medusa/src/api-v2/admin/price-lists/route.ts index 312a74906e..49da9ebfcd 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/route.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/route.ts @@ -2,7 +2,8 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { listPriceLists } from "./utils" +import { listPriceLists } from "./queries" +import { adminPriceListRemoteQueryFields } from "./query-config" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -11,7 +12,8 @@ export const GET = async ( const { limit, offset } = req.validatedQuery const [priceLists, count] = await listPriceLists({ container: req.scope, - fields: req.listConfig.select!, + apiFields: req.listConfig.select!, + remoteQueryFields: adminPriceListRemoteQueryFields, variables: { filters: req.filterableFields, order: req.listConfig.order, diff --git a/packages/medusa/src/api-v2/admin/price-lists/types.ts b/packages/medusa/src/api-v2/admin/price-lists/types.ts new file mode 100644 index 0000000000..67ea8d3402 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/types.ts @@ -0,0 +1,22 @@ +import { PriceListStatus, PriceListType } from "@medusajs/types" + +export type AdminPriceListRemoteQueryDTO = { + id: string + type?: PriceListType + description?: string + title?: string + status?: PriceListStatus + starts_at?: string + ends_at?: string + created_at?: string + updated_at?: string + deleted_at?: string + prices?: { + id: string + variant_id: string + currency_code?: string + amount?: number + min_quantity?: number + max_quantity?: number + }[] +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/utils/list-price-lists.ts b/packages/medusa/src/api-v2/admin/price-lists/utils/list-price-lists.ts deleted file mode 100644 index f2d9c886e9..0000000000 --- a/packages/medusa/src/api-v2/admin/price-lists/utils/list-price-lists.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { LinkModuleUtils, ModuleRegistrationName } from "@medusajs/modules-sdk" -import { MedusaContainer, PriceListDTO } from "@medusajs/types" -import { remoteQueryObjectFromString } from "@medusajs/utils" -import { cleanResponseData } from "../../../../utils/clean-response-data" -import { PriceListRelations, priceListRemoteQueryFields } from "../query-config" - -enum RuleAttributes { - CUSTOMER_GROUP_ID = "customer_group_id", - REGION_ID = "region_id", -} - -export async function listPriceLists({ - container, - fields, - variables, -}: { - container: MedusaContainer - fields: string[] - variables: Record -}): Promise<[PriceListDTO[], number]> { - const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY) - const customerModule = container.resolve(ModuleRegistrationName.CUSTOMER) - - const remoteQueryFields = fields.filter( - (field) => - !field.startsWith(PriceListRelations.CUSTOMER_GROUPS) && - !field.startsWith(PriceListRelations.PRICES) - ) - const customerGroupFields = fields.filter((field) => - field.startsWith(PriceListRelations.CUSTOMER_GROUPS) - ) - const pricesFields = fields.filter((field) => - field.startsWith(PriceListRelations.PRICES) - ) - - if (customerGroupFields.length) { - remoteQueryFields.push(...priceListRemoteQueryFields.customerGroupsFields) - } - - if (pricesFields.length) { - remoteQueryFields.push(...priceListRemoteQueryFields.pricesFields) - } - - const queryObject = remoteQueryObjectFromString({ - entryPoint: "price_list", - fields: remoteQueryFields, - variables, - }) - - const { - rows: priceLists, - metadata: { count }, - } = await remoteQuery(queryObject) - - if (!count) { - return [[], 0] - } - - const customerGroupIds: string[] = customerGroupFields.length - ? priceLists - .map((priceList) => priceList.price_list_rules) - .flat(1) - .filter( - (rule) => - rule.rule_type?.rule_attribute === RuleAttributes.CUSTOMER_GROUP_ID - ) - .map((rule) => rule.price_list_rule_values.map((plrv) => plrv.value)) - .flat(1) - : [] - - const customerGroups = await customerModule.listCustomerGroups( - { id: customerGroupIds }, - {} - ) - - const customerGroupIdMap = new Map(customerGroups.map((cg) => [cg.id, cg])) - - for (const priceList of priceLists) { - const priceSetMoneyAmounts = priceList.price_set_money_amounts || [] - const priceListRulesData = priceList.price_list_rules || [] - delete priceList.price_set_money_amounts - delete priceList.price_list_rules - - if (pricesFields.length) { - priceList.prices = priceSetMoneyAmounts.map((priceSetMoneyAmount) => { - const productVariant = priceSetMoneyAmount.price_set.variant - const rules = priceSetMoneyAmount.price_rules.reduce((acc, curr) => { - acc[curr.rule_type.rule_attribute] = curr.value - return acc - }, {}) - - return { - ...priceSetMoneyAmount.money_amount, - price_list_id: priceList.id, - variant_id: productVariant?.id ?? null, - region_id: rules["region_id"] ?? null, - rules, - } - }) - } - - priceList.name = priceList.title - delete priceList.title - - if (customerGroupFields.length) { - const customerGroupPriceListRule = priceListRulesData.find( - (plr) => - plr.rule_type.rule_attribute === RuleAttributes.CUSTOMER_GROUP_ID - ) - - priceList.customer_groups = - customerGroupPriceListRule?.price_list_rule_values - .map((cgr) => customerGroupIdMap.get(cgr.value)) - .filter(Boolean) || [] - } - } - - const sanitizedPriceLists = priceLists.map((priceList) => { - return cleanResponseData(priceList, fields) - }) - - return [sanitizedPriceLists, count] -} diff --git a/packages/medusa/src/api-v2/admin/products/middlewares.ts b/packages/medusa/src/api-v2/admin/products/middlewares.ts index 94a3ab3a87..85d5306e39 100644 --- a/packages/medusa/src/api-v2/admin/products/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/products/middlewares.ts @@ -1,5 +1,7 @@ +import { transformBody, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" - import { AdminGetProductsOptionsParams, AdminGetProductsParams, @@ -14,10 +16,6 @@ import { AdminPostProductsProductVariantsVariantReq, AdminPostProductsReq, } from "./validators" -import { transformBody, transformQuery } from "../../../api/middlewares" - -import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" -import { authenticate } from "../../../utils/authenticate-middleware" export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -25,7 +23,6 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/products*", middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], }, - { method: ["GET"], matcher: "/admin/products", diff --git a/packages/medusa/src/api-v2/admin/products/route.ts b/packages/medusa/src/api-v2/admin/products/route.ts index 6f5f776ff1..d042dd1c87 100644 --- a/packages/medusa/src/api-v2/admin/products/route.ts +++ b/packages/medusa/src/api-v2/admin/products/route.ts @@ -1,22 +1,59 @@ +import { createProductsWorkflow } from "@medusajs/core-flows" +import { CreateProductDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + isString, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" - -import { CreateProductDTO } from "@medusajs/types" -import { createProductsWorkflow } from "@medusajs/core-flows" -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { listPriceLists } from "../price-lists/queries" +import { AdminGetProductsParams } from "./validators" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const filterableFields: AdminGetProductsParams = { ...req.filterableFields } + const filterByPriceListIds = filterableFields.price_list_id + const priceListVariantIds: string[] = [] + + // When filtering by price_list_id, we need use the remote query to get + // the variant IDs through the price list price sets. + if (Array.isArray(filterByPriceListIds)) { + const [priceLists] = await listPriceLists({ + container: req.scope, + remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"], + apiFields: ["prices.variant_id"], + variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null }, + }) + + priceListVariantIds.push( + ...(priceLists + .map((priceList) => priceList.prices?.map((price) => price.variant_id)) + .flat(2) + .filter(isString) || []) + ) + + delete filterableFields.price_list_id + } + + if (priceListVariantIds.length) { + const existingVariantFilters = filterableFields.variants || {} + + filterableFields.variants = { + ...existingVariantFilters, + id: priceListVariantIds, + } + } const queryObject = remoteQueryObjectFromString({ entryPoint: "product", variables: { - filters: req.filterableFields, + filters: filterableFields, order: req.listConfig.order, skip: req.listConfig.skip, take: req.listConfig.take, diff --git a/packages/medusa/src/api-v2/admin/products/validators.ts b/packages/medusa/src/api-v2/admin/products/validators.ts index 8385e7a616..d34ffc5ecf 100644 --- a/packages/medusa/src/api-v2/admin/products/validators.ts +++ b/packages/medusa/src/api-v2/admin/products/validators.ts @@ -1,4 +1,5 @@ import { OperatorMap } from "@medusajs/types" +import { ProductStatus } from "@medusajs/utils" import { Transform, Type } from "class-transformer" import { IsArray, @@ -14,7 +15,6 @@ import { } from "class-validator" import { FindParams, extendedFindParamsMixin } from "../../../types/common" import { OperatorMapValidator } from "../../../types/validators/operator-map" -import { ProductStatus } from "@medusajs/utils" import { IsType } from "../../../utils" import { optionalBooleanMapper } from "../../../utils/validators/is-boolean" @@ -73,13 +73,12 @@ export class AdminGetProductsParams extends extendedFindParamsMixin({ @Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase())) is_giftcard?: boolean - // TODO: Add in next iteration - // /** - // * Filter products by their associated price lists' ID. - // */ - // @IsArray() - // @IsOptional() - // price_list_id?: string[] + /** + * Filter products by their associated price lists' ID. + */ + @IsOptional() + @IsArray() + price_list_id?: string[] /** * Filter products by their associated product collection's ID. @@ -102,6 +101,11 @@ export class AdminGetProductsParams extends extendedFindParamsMixin({ @IsOptional() type_id?: string[] + // TODO: Replace this with AdminGetProductVariantsParams when its available + @IsOptional() + @IsObject() + variants?: Record + // /** // * Filter products by their associated sales channels' ID. // */