feat(medusa): Filter product list by discount condition id (#2464)
This commit is contained in:
1 parent
9deec0fc3c
commit
8be67c734c
7 files changed
+144
-25
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa): Filter product list by discount condition id
|
||||||
@@ -10,9 +10,16 @@ const {
|
|||||||
ProductVariant,
|
ProductVariant,
|
||||||
ProductOptionValue,
|
ProductOptionValue,
|
||||||
MoneyAmount,
|
MoneyAmount,
|
||||||
|
DiscountConditionType,
|
||||||
|
DiscountConditionOperator,
|
||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
const priceListSeeder = require("../../helpers/price-list-seeder")
|
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)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -172,11 +179,7 @@ describe("/admin/products", () => {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const response = await api
|
const response = await api
|
||||||
.get("/admin/products?type_id[]=test-type", {
|
.get("/admin/products?type_id[]=test-type", adminHeaders)
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer test_token",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(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 () => {
|
it("doesn't expand collection and types", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { FilterableProductProps } from "../../../../types/product"
|
|||||||
* x-authenticated: true
|
* x-authenticated: true
|
||||||
* parameters:
|
* parameters:
|
||||||
* - (query) q {string} Query used for searching product title and description, variant title and sku, and collection title.
|
* - (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
|
* - in: query
|
||||||
* name: id
|
* name: id
|
||||||
* style: form
|
* style: form
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
|
|||||||
where: DefaultWithoutRelations["where"] & {
|
where: DefaultWithoutRelations["where"] & {
|
||||||
price_list_id?: FindOperator<PriceList>
|
price_list_id?: FindOperator<PriceList>
|
||||||
sales_channel_id?: FindOperator<SalesChannel>
|
sales_channel_id?: FindOperator<SalesChannel>
|
||||||
|
discount_condition_id?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,10 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
const sales_channels = optionsWithoutRelations?.where?.sales_channel_id
|
const sales_channels = optionsWithoutRelations?.where?.sales_channel_id
|
||||||
delete 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")
|
const qb = this.createQueryBuilder("product")
|
||||||
.select(["product.id"])
|
.select(["product.id"])
|
||||||
.skip(optionsWithoutRelations.skip)
|
.skip(optionsWithoutRelations.skip)
|
||||||
@@ -100,6 +105,15 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (optionsWithoutRelations.withDeleted) {
|
||||||
qb.withDeleted()
|
qb.withDeleted()
|
||||||
}
|
}
|
||||||
@@ -358,6 +372,16 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
.skip(cleanedOptions.skip)
|
.skip(cleanedOptions.skip)
|
||||||
.take(cleanedOptions.take)
|
.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) {
|
if (cleanedOptions.withDeleted) {
|
||||||
qb = qb.withDeleted()
|
qb = qb.withDeleted()
|
||||||
}
|
}
|
||||||
@@ -388,6 +412,10 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
delete where?.price_list_id
|
delete where?.price_list_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("discount_condition_id" in where) {
|
||||||
|
delete where?.discount_condition_id
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
where,
|
where,
|
||||||
|
|||||||
@@ -1062,7 +1062,7 @@ class CartService extends TransactionBaseService {
|
|||||||
const productsToKeep = await this.productService_
|
const productsToKeep = await this.productService_
|
||||||
.withTransaction(this.manager_)
|
.withTransaction(this.manager_)
|
||||||
.filterProductsBySalesChannel(productIds, newSalesChannelId, {
|
.filterProductsBySalesChannel(productIds, newSalesChannelId, {
|
||||||
select: ["id", "sales_channels"],
|
select: ["id"],
|
||||||
take: productIds.length,
|
take: productIds.length,
|
||||||
})
|
})
|
||||||
const productIdsToKeep = new Set<string>(
|
const productIdsToKeep = new Set<string>(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
FilterableProductProps,
|
FilterableProductProps,
|
||||||
FindProductConfig,
|
FindProductConfig,
|
||||||
ProductOptionInput,
|
ProductOptionInput,
|
||||||
|
ProductSelector,
|
||||||
UpdateProductInput,
|
UpdateProductInput,
|
||||||
} from "../types/product"
|
} from "../types/product"
|
||||||
import { buildQuery, isDefined, setMetadata } from "../utils"
|
import { buildQuery, isDefined, setMetadata } from "../utils"
|
||||||
@@ -108,7 +109,7 @@ class ProductService extends TransactionBaseService {
|
|||||||
* @return the result of the find operation
|
* @return the result of the find operation
|
||||||
*/
|
*/
|
||||||
async list(
|
async list(
|
||||||
selector: FilterableProductProps | Selector<Product> = {},
|
selector: ProductSelector,
|
||||||
config: FindProductConfig = {
|
config: FindProductConfig = {
|
||||||
relations: [],
|
relations: [],
|
||||||
skip: 0,
|
skip: 0,
|
||||||
@@ -116,20 +117,8 @@ class ProductService extends TransactionBaseService {
|
|||||||
include_discount_prices: false,
|
include_discount_prices: false,
|
||||||
}
|
}
|
||||||
): Promise<Product[]> {
|
): Promise<Product[]> {
|
||||||
const manager = this.manager_
|
const [products] = await this.listAndCount(selector, config)
|
||||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
return products
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,7 +133,7 @@ class ProductService extends TransactionBaseService {
|
|||||||
* as the second element.
|
* as the second element.
|
||||||
*/
|
*/
|
||||||
async listAndCount(
|
async listAndCount(
|
||||||
selector: FilterableProductProps | Selector<Product>,
|
selector: ProductSelector,
|
||||||
config: FindProductConfig = {
|
config: FindProductConfig = {
|
||||||
relations: [],
|
relations: [],
|
||||||
skip: 0,
|
skip: 0,
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
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 { FeatureFlagDecorators } from "../utils/feature-flag-decorators"
|
||||||
import { optionalBooleanMapper } from "../utils/validators/is-boolean"
|
import { optionalBooleanMapper } from "../utils/validators/is-boolean"
|
||||||
import { IsType } from "../utils/validators/is-type"
|
import { IsType } from "../utils/validators/is-type"
|
||||||
import { DateComparisonOperator, FindConfig } from "./common"
|
import { DateComparisonOperator, FindConfig, Selector } from "./common"
|
||||||
import { PriceListLoadConfig } from "./price-list"
|
import { PriceListLoadConfig } from "./price-list"
|
||||||
|
import { FindOperator } from "typeorm"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Level DTOs + Validation rules
|
* API Level DTOs + Validation rules
|
||||||
@@ -67,6 +74,10 @@ export class FilterableProductProps {
|
|||||||
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()])
|
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()])
|
||||||
sales_channel_id?: string[]
|
sales_channel_id?: string[]
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
discount_condition_id?: string
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DateComparisonOperator)
|
@Type(() => DateComparisonOperator)
|
||||||
@@ -83,6 +94,15 @@ export class FilterableProductProps {
|
|||||||
deleted_at?: DateComparisonOperator
|
deleted_at?: DateComparisonOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProductSelector =
|
||||||
|
| FilterableProductProps
|
||||||
|
| (Selector<Product> & {
|
||||||
|
q?: string
|
||||||
|
discount_condition_id?: string
|
||||||
|
price_list_id?: string[] | FindOperator<PriceList>
|
||||||
|
sales_channel_id?: string[] | FindOperator<SalesChannel>
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service Level DTOs
|
* Service Level DTOs
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user