diff --git a/.changeset/tidy-tigers-invite.md b/.changeset/tidy-tigers-invite.md new file mode 100644 index 0000000000..ae84b32f91 --- /dev/null +++ b/.changeset/tidy-tigers-invite.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Filter product list by discount condition id diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index eee503ae84..6b7bdea95b 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -10,9 +10,16 @@ const { ProductVariant, ProductOptionValue, MoneyAmount, + DiscountConditionType, + DiscountConditionOperator, } = require("@medusajs/medusa") const priceListSeeder = require("../../helpers/price-list-seeder") -const { simpleProductFactory } = require("../../factories") +const { + simpleProductFactory, + simpleDiscountFactory, +} = require("../../factories") +const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist") +const { IdMap } = require("medusa-test-utils") jest.setTimeout(50000) @@ -172,11 +179,7 @@ describe("/admin/products", () => { const api = useApi() const response = await api - .get("/admin/products?type_id[]=test-type", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/products?type_id[]=test-type", adminHeaders) .catch((err) => { console.log(err) }) @@ -192,6 +195,79 @@ describe("/admin/products", () => { ) }) + it("returns a list of products filtered by discount condition id", async () => { + const api = useApi() + + const resProd = await api.get("/admin/products", adminHeaders) + + const prod1 = resProd.data.products[0] + const prod2 = resProd.data.products[2] + + const buildDiscountData = (code, conditionId, products) => { + return { + code, + rule: { + type: DiscountRuleType.PERCENTAGE, + value: 10, + allocation: AllocationType.TOTAL, + conditions: [ + { + id: conditionId, + type: DiscountConditionType.PRODUCTS, + operator: DiscountConditionOperator.IN, + product_tags: products, + }, + ], + }, + } + } + + const discountConditionId = IdMap.getId("discount-condition-prod-1") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-1", discountConditionId, [prod1.id]) + ) + + const discountConditionId2 = IdMap.getId("discount-condition-prod-2") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-2", discountConditionId2, [prod2.id]) + ) + + let res = await api.get( + `/admin/products?discount_condition_id=${discountConditionId}`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data.products).toHaveLength(1) + expect(res.data.products).toEqual( + expect.arrayContaining([expect.objectContaining({ id: prod1.id })]) + ) + + res = await api.get( + `/admin/products?discount_condition_id=${discountConditionId2}`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data.products).toHaveLength(1) + expect(res.data.products).toEqual( + expect.arrayContaining([expect.objectContaining({ id: prod2.id })]) + ) + + res = await api.get(`/admin/products`, adminHeaders) + + expect(res.status).toEqual(200) + expect(res.data.products).toHaveLength(5) + expect(res.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: prod1.id }), + expect.objectContaining({ id: prod2.id }), + ]) + ) + }) + it("doesn't expand collection and types", async () => { const api = useApi() diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index 002be0b63e..a091224cca 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -14,6 +14,7 @@ import { FilterableProductProps } from "../../../../types/product" * x-authenticated: true * parameters: * - (query) q {string} Query used for searching product title and description, variant title and sku, and collection title. + * - (query) discount_condition_id {string} The discount condition id on which to filter the product. * - in: query * name: id * style: form diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index dee02702ad..280eb6be54 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -26,6 +26,7 @@ export type FindWithoutRelationsOptions = DefaultWithoutRelations & { where: DefaultWithoutRelations["where"] & { price_list_id?: FindOperator sales_channel_id?: FindOperator + discount_condition_id?: string } } @@ -53,6 +54,10 @@ export class ProductRepository extends Repository { const sales_channels = optionsWithoutRelations?.where?.sales_channel_id delete optionsWithoutRelations?.where?.sales_channel_id + const discount_condition_id = + optionsWithoutRelations?.where?.discount_condition_id + delete optionsWithoutRelations?.where?.discount_condition_id + const qb = this.createQueryBuilder("product") .select(["product.id"]) .skip(optionsWithoutRelations.skip) @@ -100,6 +105,15 @@ export class ProductRepository extends Repository { ) } + if (discount_condition_id) { + qb.innerJoin( + "discount_condition_product", + "dc_product", + `dc_product.product_id = product.id AND dc_product.condition_id = :dcId`, + { dcId: discount_condition_id } + ) + } + if (optionsWithoutRelations.withDeleted) { qb.withDeleted() } @@ -358,6 +372,16 @@ export class ProductRepository extends Repository { .skip(cleanedOptions.skip) .take(cleanedOptions.take) + const discountConditionId = options.where.discount_condition_id + if (discountConditionId) { + qb.innerJoin( + "discount_condition_product", + "dc_product", + `dc_product.product_id = product.id AND dc_product.condition_id = :dcId`, + { dcId: discountConditionId } + ) + } + if (cleanedOptions.withDeleted) { qb = qb.withDeleted() } @@ -388,6 +412,10 @@ export class ProductRepository extends Repository { delete where?.price_list_id } + if ("discount_condition_id" in where) { + delete where?.discount_condition_id + } + return { ...options, where, diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 46c791f00a..81ec928be4 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1062,7 +1062,7 @@ class CartService extends TransactionBaseService { const productsToKeep = await this.productService_ .withTransaction(this.manager_) .filterProductsBySalesChannel(productIds, newSalesChannelId, { - select: ["id", "sales_channels"], + select: ["id"], take: productIds.length, }) const productIdsToKeep = new Set( diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 3d071917d4..7c33aab648 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -28,6 +28,7 @@ import { FilterableProductProps, FindProductConfig, ProductOptionInput, + ProductSelector, UpdateProductInput, } from "../types/product" import { buildQuery, isDefined, setMetadata } from "../utils" @@ -108,7 +109,7 @@ class ProductService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: FilterableProductProps | Selector = {}, + selector: ProductSelector, config: FindProductConfig = { relations: [], skip: 0, @@ -116,20 +117,8 @@ class ProductService extends TransactionBaseService { include_discount_prices: false, } ): Promise { - const manager = this.manager_ - const productRepo = manager.getCustomRepository(this.productRepository_) - - const { q, query, relations } = this.prepareListQuery_(selector, config) - if (q) { - const [products] = await productRepo.getFreeTextSearchResultsAndCount( - q, - query, - relations - ) - return products - } - - return await productRepo.findWithRelations(relations, query) + const [products] = await this.listAndCount(selector, config) + return products } /** @@ -144,7 +133,7 @@ class ProductService extends TransactionBaseService { * as the second element. */ async listAndCount( - selector: FilterableProductProps | Selector, + selector: ProductSelector, config: FindProductConfig = { relations: [], skip: 0, diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index 2c19255e60..4322c8ec87 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -8,12 +8,19 @@ import { ValidateNested, } from "class-validator" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" -import { Product, ProductOptionValue, ProductStatus } from "../models" +import { + PriceList, + Product, + ProductOptionValue, + ProductStatus, + SalesChannel, +} from "../models" import { FeatureFlagDecorators } from "../utils/feature-flag-decorators" import { optionalBooleanMapper } from "../utils/validators/is-boolean" import { IsType } from "../utils/validators/is-type" -import { DateComparisonOperator, FindConfig } from "./common" +import { DateComparisonOperator, FindConfig, Selector } from "./common" import { PriceListLoadConfig } from "./price-list" +import { FindOperator } from "typeorm" /** * API Level DTOs + Validation rules @@ -67,6 +74,10 @@ export class FilterableProductProps { @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()]) sales_channel_id?: string[] + @IsString() + @IsOptional() + discount_condition_id?: string + @IsOptional() @ValidateNested() @Type(() => DateComparisonOperator) @@ -83,6 +94,15 @@ export class FilterableProductProps { deleted_at?: DateComparisonOperator } +export type ProductSelector = + | FilterableProductProps + | (Selector & { + q?: string + discount_condition_id?: string + price_list_id?: string[] | FindOperator + sales_channel_id?: string[] | FindOperator + }) + /** * Service Level DTOs */