diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index 0c03141bf2..e7aae7f444 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -794,9 +794,17 @@ describe("/admin/price-lists", () => { await simplePriceListFactory(dbConnection, { id: "test-list", + customer_groups: ["test-group"], prices: [ - { variant_id: "test-variant-1", currency_code: "usd", amount: 100 }, - { variant_id: "test-variant-4", currency_code: "usd", amount: 100 }, + { variant_id: "test-variant-1", currency_code: "usd", amount: 150 }, + { variant_id: "test-variant-4", currency_code: "usd", amount: 150 }, + ], + }) + await simplePriceListFactory(dbConnection, { + id: "test-list-2", + prices: [ + { variant_id: "test-variant-1", currency_code: "usd", amount: 200 }, + { variant_id: "test-variant-4", currency_code: "usd", amount: 200 }, ], }) } catch (err) { @@ -810,7 +818,7 @@ describe("/admin/price-lists", () => { await db.teardown() }) - it("lists only product 1, 2", async () => { + it("lists only product 1, 2 with price list prices", async () => { const api = useApi() const response = await api @@ -826,8 +834,50 @@ describe("/admin/price-lists", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(2) expect(response.data.products).toEqual([ - expect.objectContaining({ id: "test-prod-1" }), - expect.objectContaining({ id: "test-prod-2" }), + expect.objectContaining({ + id: "test-prod-1", + variants: [ + expect.objectContaining({ + id: "test-variant-1", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + expect.objectContaining({ + id: "test-variant-2", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + ], + }), + expect.objectContaining({ + id: "test-prod-2", + variants: [ + expect.objectContaining({ + id: "test-variant-3", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ], + }), + expect.objectContaining({ + id: "test-variant-4", + prices: [ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ], + }), + ], + }), ]) }) diff --git a/integration-tests/api/factories/simple-price-list-factory.ts b/integration-tests/api/factories/simple-price-list-factory.ts index 439b35deec..a20380d3b0 100644 --- a/integration-tests/api/factories/simple-price-list-factory.ts +++ b/integration-tests/api/factories/simple-price-list-factory.ts @@ -3,6 +3,7 @@ import { MoneyAmount, PriceListType, PriceListStatus, + CustomerGroup, } from "@medusajs/medusa" import faker from "faker" import { Connection } from "typeorm" @@ -38,6 +39,28 @@ export const simplePriceListFactory = async ( const manager = connection.manager const listId = data.id || `simple-price-list-${Math.random() * 1000}` + + let customerGroups = [] + if (typeof data.customer_groups !== "undefined") { + await manager + .createQueryBuilder() + .insert() + .into(CustomerGroup) + .values( + data.customer_groups.map((group) => ({ + id: group, + name: faker.company.companyName(), + })) + ) + .orIgnore() + .execute() + + customerGroups = await manager.findByIds( + CustomerGroup, + data.customer_groups + ) + } + const toCreate = { id: listId, name: data.name || faker.commerce.productName(), @@ -46,6 +69,7 @@ export const simplePriceListFactory = async ( type: data.type || PriceListType.OVERRIDE, starts_at: data.starts_at || null, ends_at: data.ends_at || null, + customer_groups: customerGroups, } const toSave = manager.create(PriceList, toCreate) diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 5cbe0abba6..21a3f73b66 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -1,5 +1,5 @@ import { Type } from "class-transformer" -import { omit } from "lodash" +import { omit, pickBy } from "lodash" import { IsArray, IsBoolean, @@ -19,6 +19,9 @@ import { defaultAdminProductRelations, } from "../products" import listAndCount from "../../../../controllers/products/admin-list-products" +import { MedusaError } from "medusa-core-utils" +import { getListConfig } from "../../../../utils/get-query-config" +import PriceListService from "../../../../services/price-list" /** * @oas [get] /price-lists/:id/products @@ -78,7 +81,7 @@ export default async (req, res) => { req.query.price_list_id = [id] - const filterableFields: FilterableProductProps = omit(req.query, [ + const query: FilterableProductProps = omit(req.query, [ "limit", "offset", "expand", @@ -86,23 +89,71 @@ export default async (req, res) => { "order", ]) - const result = await listAndCount( - req.scope, - filterableFields, - {}, - { - limit: validatedParams.limit ?? 50, - offset: validatedParams.offset ?? 0, - expand: validatedParams.expand, - fields: validatedParams.fields, - order: validatedParams.order, - allowedFields: allowedAdminProductFields, - defaultFields: defaultAdminProductFields as (keyof Product)[], - defaultRelations: defaultAdminProductRelations, - } + const limit = validatedParams.limit ?? 50 + const offset = validatedParams.offset ?? 0 + const expand = validatedParams.expand + const fields = validatedParams.fields + const order = validatedParams.order + const allowedFields = allowedAdminProductFields + const defaultFields = defaultAdminProductFields as (keyof Product)[] + const defaultRelations = defaultAdminProductRelations.filter( + (r) => r !== "variants.prices" ) - res.json(result) + const priceListService: PriceListService = + req.scope.resolve("priceListService") + + let includeFields: (keyof Product)[] | undefined + if (fields) { + includeFields = fields.split(",") as (keyof Product)[] + } + + let expandFields: string[] | undefined + if (expand) { + expandFields = expand.split(",") + } + + let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined + if (typeof order !== "undefined") { + let orderField = order + if (order.startsWith("-")) { + const [, field] = order.split("-") + orderField = field + orderBy = { [field]: "DESC" } + } else { + orderBy = { [order]: "ASC" } + } + + if (!(allowedFields || []).includes(orderField)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Order field must be a valid product field" + ) + } + } + + const listConfig = getListConfig( + defaultFields ?? [], + defaultRelations ?? [], + includeFields, + expandFields, + limit, + offset, + orderBy + ) + + const [products, count] = await priceListService.listProducts( + id, + pickBy(query, (val) => typeof val !== "undefined"), + listConfig + ) + + res.json({ + products, + count, + offset, + limit, + }) } enum ProductStatus { diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 496562a962..716db232cf 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -116,6 +116,24 @@ export class MoneyAmountRepository extends Repository { .execute() } + public async findManyForVariantInPriceList( + variant_id: string, + price_list_id: string + ): Promise<[MoneyAmount[], number]> { + const qb = this.createQueryBuilder("ma") + .leftJoinAndSelect("ma.price_list", "price_list") + .where("ma.variant_id = :variant_id", { variant_id }) + .andWhere( + new Brackets((qb) => { + qb.where("ma.price_list_id = :price_list_id", { + price_list_id, + }).orWhere("ma.price_list_id IS NULL") + }) + ) + + return await qb.getManyAndCount() + } + public async findManyForVariantInRegion( variant_id: string, region_id?: string, diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index 0312b6e81e..6c96e8508e 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { EntityManager } from "typeorm" import { CustomerGroupService } from "." +import { Product } from "../models" import { CustomerGroup } from "../models/customer-group" import { PriceList } from "../models/price-list" import { MoneyAmountRepository } from "../repositories/money-amount" @@ -14,10 +15,12 @@ import { UpdatePriceListInput, } from "../types/price-list" import { formatException } from "../utils/exception-formatter" +import ProductService from "./product" type PriceListConstructorProps = { manager: EntityManager customerGroupService: CustomerGroupService + productService: ProductService priceListRepository: typeof PriceListRepository moneyAmountRepository: typeof MoneyAmountRepository } @@ -29,18 +32,21 @@ type PriceListConstructorProps = { class PriceListService extends BaseService { private manager_: EntityManager private customerGroupService_: CustomerGroupService + private productService_: ProductService private priceListRepo_: typeof PriceListRepository private moneyAmountRepo_: typeof MoneyAmountRepository constructor({ manager, customerGroupService, + productService, priceListRepository, moneyAmountRepository, }: PriceListConstructorProps) { super() this.manager_ = manager this.customerGroupService_ = customerGroupService + this.productService_ = productService this.priceListRepo_ = priceListRepository this.moneyAmountRepo_ = moneyAmountRepository } @@ -53,6 +59,7 @@ class PriceListService extends BaseService { const cloned = new PriceListService({ manager: transactionManager, customerGroupService: this.customerGroupService_, + productService: this.productService_, priceListRepository: this.priceListRepo_, moneyAmountRepository: this.moneyAmountRepo_, }) @@ -276,6 +283,50 @@ class PriceListService extends BaseService { await priceListRepo.save(priceList) } + + async listProducts( + priceListId: string, + selector = {}, + config: FindConfig = { + relations: [], + skip: 0, + take: 20, + } + ): Promise<[Product[], number]> { + return await this.atomicPhase_(async (manager: EntityManager) => { + const [products, count] = await this.productService_.listAndCount( + selector, + config + ) + + const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_) + + const productsWithPrices = await Promise.all( + products.map(async (p) => { + if (p.variants?.length) { + p.variants = await Promise.all( + p.variants.map(async (v) => { + const [prices] = + await moneyAmountRepo.findManyForVariantInPriceList( + v.id, + priceListId + ) + + return { + ...v, + prices, + } + }) + ) + } + + return p + }) + ) + + return [productsWithPrices, count] + }) + } } export default PriceListService