diff --git a/.changeset/silent-forks-cough.md b/.changeset/silent-forks-cough.md new file mode 100644 index 0000000000..553a274e5d --- /dev/null +++ b/.changeset/silent-forks-cough.md @@ -0,0 +1,7 @@ +--- +"@medusajs/pricing": patch +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat(pricing,medusa,utils): added list + get endpoints for price lists diff --git a/integration-tests/api/__tests__/admin/order/order.js b/integration-tests/api/__tests__/admin/order/order.js index bf4502d014..96fc20b7bf 100644 --- a/integration-tests/api/__tests__/admin/order/order.js +++ b/integration-tests/api/__tests__/admin/order/order.js @@ -2523,6 +2523,9 @@ describe("/admin/orders", () => { refundable_amount: 10000, gift_card_total: 0, gift_card_tax_total: 0, + items: [{ refundable: 7200 }], + claims: [], + swaps: [], }) }) diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index 44828e5776..01a9b6d758 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -290,27 +290,29 @@ describe("/store/carts", () => { "/store/orders/order_test?fields=status&expand=billing_address" ) - expect(Object.keys(response.data.order)).toEqual([ - // fields - "status", + expect(Object.keys(response.data.order).sort()).toEqual( + [ + // fields + "status", - // selected relations - "billing_address", + // selected relations + "billing_address", - // totals - "shipping_total", - "discount_total", - "tax_total", - "refunded_total", - "total", - "subtotal", - "paid_total", - "refundable_amount", - "gift_card_total", - "gift_card_tax_total", - "item_tax_total", - "shipping_tax_total", - ]) + // totals + "shipping_total", + "discount_total", + "tax_total", + "refunded_total", + "total", + "subtotal", + "paid_total", + "refundable_amount", + "gift_card_total", + "gift_card_tax_total", + "item_tax_total", + "shipping_tax_total", + ].sort() + ) }) it("looks up order", async () => { 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 new file mode 100644 index 0000000000..a74413275d --- /dev/null +++ b/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts @@ -0,0 +1,256 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + ICustomerModuleService, + IPricingModuleService, + IProductModuleService, + IRegionModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import adminSeeder from "../../../../helpers/admin-seeder" +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" }, +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Admin: Price Lists API", () => { + let appContainer + let product + let variant + let region + let customerGroup + let pricingModule: IPricingModuleService + let productModule: IProductModuleService + let customerModule: ICustomerModuleService + let regionModule: IRegionModuleService + + beforeAll(async () => { + appContainer = getContainer() + pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING) + productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT) + customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER) + regionModule = appContainer.resolve(ModuleRegistrationName.REGION) + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + customerGroup = await customerModule.createCustomerGroup({ + name: "VIP", + }) + region = await regionModule.create({ name: "US", currency_code: "USD" }) + ;[product] = await productModule.create([{ title: "test product" }]) + + await pricingModule.createRuleTypes([ + { name: "Customer Group ID", rule_attribute: "customer_group_id" }, + { name: "Region ID", rule_attribute: "region_id" }, + ]) + + const [productOption] = await productModule.createOptions([ + { title: "Test option 1", product_id: product.id }, + ]) + + ;[variant] = await productModule.createVariants([ + { + product_id: product.id, + title: "test product variant", + options: [{ value: "test", option_id: productOption.id }], + }, + ]) + }) + + describe("GET /admin/price-lists", () => { + it("should get price list and its money amounts with variants", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + 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, + }, + ], + rules: { + customer_group_id: [customerGroup.id], + }, + }, + ]) + + 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`, + 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), + prices: [ + { + id: expect.any(String), + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + variant_id: expect.any(String), + region_id: null, + }, + ], + 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 () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + 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, + }, + ], + }, + ]) + + 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, + }, + ], + }, + ]) + + let response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + 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", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + }) + ) + + response = await api.get( + `/admin/price-lists/${priceList.id}?fields=id,prices.id,prices.amount`, + adminHeaders + ) + + expect(response.data.price_list).toEqual({ + id: expect.any(String), + prices: [ + { + id: expect.any(String), + amount: 5000, + }, + ], + }) + }) + + it("should throw an error when price list is not found", async () => { + const error = await api + .get(`/admin/price-lists/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(error.response.status).toBe(404) + expect(error.response.data).toEqual({ + type: "not_found", + message: "Price list with id: does-not-exist was not found", + }) + }) + }) + }) + }, +}) diff --git a/packages/link-modules/src/definitions/product-variant-price-set.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts index e90b13efbf..19458f3c28 100644 --- a/packages/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -56,6 +56,9 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = { foreignKey: "id", alias: "variant_link", }, + fieldAlias: { + variant: "variant_link.variant", + }, }, ], } 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 new file mode 100644 index 0000000000..8d5998db08 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/[id]/route.ts @@ -0,0 +1,31 @@ +import { MedusaError } from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { listPriceLists } from "../utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + const [[priceList], count] = await listPriceLists({ + container: req.scope, + fields: req.retrieveConfig.select!, + variables: { + filters: { id }, + skip: 0, + take: 1, + }, + }) + + if (count === 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Price list with id: ${id} was not found` + ) + } + + res.status(200).json({ price_list: priceList }) +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts new file mode 100644 index 0000000000..f93250a61d --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts @@ -0,0 +1,30 @@ +import { transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetPriceListsParams, + AdminGetPriceListsPriceListParams, +} from "./validators" + +export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/admin/price-lists", + middlewares: [ + transformQuery( + AdminGetPriceListsParams, + QueryConfig.adminListTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/price-lists/:id", + middlewares: [ + transformQuery( + AdminGetPriceListsPriceListParams, + QueryConfig.adminRetrieveTransformQueryConfig + ), + ], + }, +] 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 new file mode 100644 index 0000000000..7554e40754 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/query-config.ts @@ -0,0 +1,65 @@ +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 defaultAdminPriceListFields = [ + ...priceListRemoteQueryFields.fields, + "name", +] + +export const defaultAdminPriceListRelations = [] + +export const allowedAdminPriceListRelations = [ + PriceListRelations.CUSTOMER_GROUPS, + PriceListRelations.PRICES, +] + +export const adminListTransformQueryConfig = { + defaultLimit: 50, + defaultFields: defaultAdminPriceListFields, + defaultRelations: defaultAdminPriceListRelations, + allowedRelations: allowedAdminPriceListRelations, + isList: true, +} + +export const adminRetrieveTransformQueryConfig = { + defaultFields: defaultAdminPriceListFields, + defaultRelations: defaultAdminPriceListRelations, + allowedRelations: allowedAdminPriceListRelations, + isList: false, +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/route.ts b/packages/medusa/src/api-v2/admin/price-lists/route.ts new file mode 100644 index 0000000000..312a74906e --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/route.ts @@ -0,0 +1,29 @@ +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { listPriceLists } from "./utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { limit, offset } = req.validatedQuery + const [priceLists, count] = await listPriceLists({ + container: req.scope, + fields: req.listConfig.select!, + variables: { + filters: req.filterableFields, + order: req.listConfig.order, + skip: req.listConfig.skip, + take: req.listConfig.take, + }, + }) + + res.json({ + count, + price_lists: priceLists, + offset, + limit, + }) +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/utils/index.ts b/packages/medusa/src/api-v2/admin/price-lists/utils/index.ts new file mode 100644 index 0000000000..56ce79ca4c --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/utils/index.ts @@ -0,0 +1 @@ +export * from "./list-price-lists" 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 new file mode 100644 index 0000000000..f2d9c886e9 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/utils/list-price-lists.ts @@ -0,0 +1,123 @@ +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/price-lists/validators.ts b/packages/medusa/src/api-v2/admin/price-lists/validators.ts new file mode 100644 index 0000000000..1fb1b262dd --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/validators.ts @@ -0,0 +1,4 @@ +import { FindParams } from "../../../types/common" + +export class AdminGetPriceListsParams extends FindParams {} +export class AdminGetPriceListsPriceListParams extends FindParams {} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index aaa4345507..bf0434a01d 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -5,6 +5,7 @@ import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" +import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares" import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" @@ -43,5 +44,6 @@ export const config: MiddlewaresConfig = { ...adminCurrencyRoutesMiddlewares, ...storeCurrencyRoutesMiddlewares, ...adminProductRoutesMiddlewares, + ...adminPriceListsRoutesMiddlewares, ], } diff --git a/packages/medusa/src/api/middlewares/transform-query.ts b/packages/medusa/src/api/middlewares/transform-query.ts index 58b2b520e3..cb867cd6c4 100644 --- a/packages/medusa/src/api/middlewares/transform-query.ts +++ b/packages/medusa/src/api/middlewares/transform-query.ts @@ -1,17 +1,17 @@ -import { NextFunction, Request, Response } from "express" -import { ClassConstructor } from "../../types/global" -import { validator } from "../../utils/validator" +import { buildSelects, objectToStringPath } from "@medusajs/utils" import { ValidatorOptions } from "class-validator" -import { default as normalizeQuery } from "./normalized-query" +import { NextFunction, Request, Response } from "express" +import { omit } from "lodash" +import { BaseEntity } from "../../interfaces" +import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common" +import { ClassConstructor } from "../../types/global" +import { removeUndefinedProperties } from "../../utils" import { prepareListQuery, prepareRetrieveQuery, } from "../../utils/get-query-config" -import { BaseEntity } from "../../interfaces" -import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common" -import { omit } from "lodash" -import { removeUndefinedProperties } from "../../utils" -import { buildSelects, objectToStringPath } from "@medusajs/utils" +import { validator } from "../../utils/validator" +import { default as normalizeQuery } from "./normalized-query" /** * Middleware that transform the query input for the admin end points diff --git a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts index 374e0d0a99..8acc178bd4 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts @@ -1,9 +1,6 @@ -import { MedusaContainer } from "@medusajs/types" -import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" -import { getPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists/{id} @@ -87,22 +84,13 @@ import { getPriceListPricingModule } from "./modules-queries" export default async (req, res) => { const { id } = req.params - const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const priceListService: PriceListService = req.scope.resolve("priceListService") - let priceList - - if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - priceList = await getPriceListPricingModule(id, { - container: req.scope as MedusaContainer, - }) - } else { - priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) - } + const priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) res.status(200).json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts index 2abca6686f..1bbfcb3651 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts @@ -1,11 +1,8 @@ -import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { Type } from "class-transformer" import { IsNumber, IsOptional, IsString } from "class-validator" import { Request } from "express" import PriceListService from "../../../../services/price-list" import { FilterablePriceListProps } from "../../../../types/price-list" -import { MedusaContainer } from "@medusajs/types" -import { listAndCountPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists @@ -190,27 +187,14 @@ import { listAndCountPriceListPricingModule } from "./modules-queries" * $ref: "#/components/responses/500_error" */ export default async (req: Request, res) => { - const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") - const validated = req.validatedQuery - let priceLists - let count + const priceListService: PriceListService = + req.scope.resolve("priceListService") - if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - [priceLists, count] = await listAndCountPriceListPricingModule({ - filters: req.filterableFields, - listConfig: req.listConfig, - container: req.scope as MedusaContainer, - }) - } else { - const priceListService: PriceListService = - req.scope.resolve("priceListService") - - ;[priceLists, count] = await priceListService.listAndCount( - req.filterableFields, - req.listConfig - ) - } + const [priceLists, count] = await priceListService.listAndCount( + req.filterableFields, + req.listConfig + ) res.json({ price_lists: priceLists, diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts index 3f13f6fb47..2babc6334d 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts @@ -1,6 +1,6 @@ import { MedusaContainer } from "@medusajs/types" -import { PriceList } from "../../../../../models" import { MedusaError } from "medusa-core-utils" +import { PriceList } from "../../../../../models" import { listAndCountPriceListPricingModule } from "./list-and-count-price-lists" export async function getPriceListPricingModule( @@ -12,9 +12,7 @@ export async function getPriceListPricingModule( } ): Promise { const [priceLists, count] = await listAndCountPriceListPricingModule({ - filters: { - id: [id], - }, + filters: { id: [id] }, container, }) diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts index b310378f3c..236edefaf7 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts @@ -1,7 +1,7 @@ +import { LinkModuleUtils, ModuleRegistrationName } from "@medusajs/modules-sdk" import { FilterablePriceListProps, MedusaContainer } from "@medusajs/types" -import { FindConfig } from "../../../../../types/common" import { CustomerGroup, MoneyAmount, PriceList } from "../../../../../models" -import { CustomerGroupService } from "../../../../../services" +import { FindConfig } from "../../../../../types/common" import { defaultAdminPriceListRemoteQueryObject } from "../index" export async function listAndCountPriceListPricingModule({ @@ -13,10 +13,8 @@ export async function listAndCountPriceListPricingModule({ filters?: FilterablePriceListProps listConfig?: FindConfig }): Promise<[PriceList[], number]> { - const remoteQuery = container.resolve("remoteQuery") - const customerGroupService: CustomerGroupService = container.resolve( - "customerGroupService" - ) + const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY) + const customerModule = container.resolve(ModuleRegistrationName.CUSTOMER) const query = { price_list: { @@ -44,17 +42,8 @@ export async function listAndCountPriceListPricingModule({ ) .flat(2) - const priceListCustomerGroups = await customerGroupService.list( - { id: customerGroupIds }, - {} - ) - - const customerGroupIdCustomerGroupMap = new Map( - priceListCustomerGroups.map((customerGroup) => [ - customerGroup.id, - customerGroup, - ]) - ) + const customerGroups = await customerModule.list({ id: customerGroupIds }, {}) + const customerGroupIdMap = new Map(customerGroups.map((cg) => [cg.id, cg])) for (const priceList of priceLists) { const priceSetMoneyAmounts = priceList.price_set_money_amounts || [] @@ -83,27 +72,14 @@ export async function listAndCountPriceListPricingModule({ priceList.name = priceList.title delete priceList.title - const customerGroupPriceListRule = priceListRulesData.find( + const customerGroupRule = priceListRulesData.find( (plr) => plr.rule_type.rule_attribute === "customer_group_id" ) - if ( - customerGroupPriceListRule && - customerGroupPriceListRule?.price_list_rule_values - ) { - priceList.customer_groups = - customerGroupPriceListRule?.price_list_rule_values - .map((customerGroupRule) => - customerGroupIdCustomerGroupMap.get(customerGroupRule.value) - ) - .filter( - ( - customerGroup: CustomerGroup | undefined - ): customerGroup is CustomerGroup => !!customerGroup - ) - } else { - priceList.customer_groups = [] - } + priceList.customer_groups = + customerGroupRule?.price_list_rule_values + .map((cgr) => customerGroupIdMap.get(cgr.value)) + .filter((cg): cg is CustomerGroup => !!cg) || [] } return [priceLists, count] diff --git a/packages/medusa/src/utils/clean-response-data.ts b/packages/medusa/src/utils/clean-response-data.ts index b9ce0f4454..66af60de0f 100644 --- a/packages/medusa/src/utils/clean-response-data.ts +++ b/packages/medusa/src/utils/clean-response-data.ts @@ -1,4 +1,4 @@ -import { pick } from "lodash" +import { pickDeep } from "@medusajs/utils" import { omitDeep } from "./omit-deep" // TODO: once the legacy totals decoration will be removed. @@ -52,9 +52,9 @@ function cleanResponseData( fields = [...fieldsSet] - arrayData = arrayData.map((record) => - pick(omitDeep(record, EXCLUDED_FIELDS), fields) - ) + arrayData = arrayData.map((record) => { + return pickDeep(omitDeep(record, EXCLUDED_FIELDS), fields) + }) return (isDataArray ? arrayData : arrayData[0]) as T extends [] ? Partial[] diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts index b138d61f16..6cc44bec8c 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts @@ -1,11 +1,11 @@ import { MikroOrmWrapper } from "../../../utils" +import { Modules } from "@medusajs/modules-sdk" import { IPricingModuleService } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createPriceLists } from "../../../__fixtures__/price-list" import { createPriceSets } from "../../../__fixtures__/price-set" -import { Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" jest.setTimeout(30000) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 0b521bae37..ca8de7ae85 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -31,8 +31,10 @@ export * from "./medusa-container" export * from "./object-from-string-path" export * from "./object-to-string-path" export * from "./optional-numeric-serializer" +export * from "./pick-deep" export * from "./pick-value-from-object" export * from "./plurailze" +export * from "./prefix-array-items" export * from "./promise-all" export * from "./remote-query-object-from-string" export * from "./remote-query-object-to-string" diff --git a/packages/utils/src/common/pick-deep.ts b/packages/utils/src/common/pick-deep.ts new file mode 100644 index 0000000000..cca15cb683 --- /dev/null +++ b/packages/utils/src/common/pick-deep.ts @@ -0,0 +1,63 @@ +import { isObject } from "./is-object" + +export function pickDeep( + input: object, + fields: Array, + prefix: string = "" +): T { + if (!input) { + return input + } + + return Object.entries(input).reduce((nextInput, [key, value]) => { + const fieldKey = withPrefix(key, prefix) + const fieldMatches = fields.includes(fieldKey) + const partialKeyMatch = + fields.filter((field) => field.toString().startsWith(`${fieldKey}.`)) + .length > 0 + + const valueIsObject = isObject(value) + const valueIsArray = Array.isArray(value) + + if (fieldMatches && (valueIsObject || valueIsArray)) { + nextInput[key] = value + + return nextInput + } + + if (!fieldMatches && !partialKeyMatch) { + return nextInput + } + + if (valueIsArray) { + nextInput[key] = value.map((arrItem) => { + if (isObject(arrItem)) { + return pickDeep(arrItem, fields, withPrefix(key, prefix)) + } + return arrItem + }) + + return nextInput + } else if (valueIsObject) { + if (Object.keys(value).length) { + nextInput[key] = pickDeep(value, fields, withPrefix(key, prefix)) + } + + return nextInput + } + + if (fieldMatches) { + nextInput[key] = value + } + + return nextInput + }, {} as T) +} + +function withPrefix(key: string, prefix: string): string { + if (prefix.length) { + return `${prefix}.${key}` + } else { + return key + } +} diff --git a/packages/utils/src/common/prefix-array-items.ts b/packages/utils/src/common/prefix-array-items.ts new file mode 100644 index 0000000000..30abfeb139 --- /dev/null +++ b/packages/utils/src/common/prefix-array-items.ts @@ -0,0 +1,8 @@ +/** + * Prefixes an array of strings with a specified string + * @param array + * @param prefix + */ +export function prefixArrayItems(array: string[], prefix: string): string[] { + return array.map((arrEl) => `${prefix}${arrEl}`) +}