add max purchase count

This commit is contained in:
2026-02-14 15:52:43 +03:30
parent 942094118c
commit eb4d19ab81
7 changed files with 49 additions and 4 deletions
+1
View File
@@ -705,6 +705,7 @@ export const enum CartMessage {
PRODUCT_NOT_BELONGS_TO_SHOP = 'محصول به این فروشگاه تعلق ندارد',
PRODUCT_NO_INVENTORY = 'محصول موجودی ندارد',
INSUFFICIENT_STOCK = 'موجودی کافی نیست',
MAX_PURCHASE_EXCEEDED = 'تعداد درخواستی بیش از حداکثر مجاز خرید این محصول است',
USER_NOT_FOUND = 'کاربر یافت نشد',
ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
SHOP_NOT_FOUND = 'فروشگاه یافت نشد',
@@ -68,6 +68,7 @@ export class CartItemService {
shopId,
quantityToAdd,
existingQuantityInCart,
cart,
);
if (index < 0) {
@@ -29,14 +29,16 @@ export class CartValidationService {
) { }
/**
* Validate variant exists, belongs to shop, and has sufficient stock.
* Validate variant exists, belongs to shop, has sufficient stock, and respects max purchase per product.
* @param existingQuantityInCart - current quantity of this variant already in cart (for add/increment)
* @param cart - current cart to compute total product quantity (for max purchase validation)
*/
async validateAndGetVariant(
variantId: string,
shopId: string,
quantityToAdd: number,
existingQuantityInCart: number = 0,
cart?: { items: { variantId: string; quantity: number }[] },
): Promise<Variant> {
const variant = await this.productService.findOrFailVariant(variantId);
@@ -51,6 +53,20 @@ export class CartValidationService {
}
}
const maxPurchase = variant.product.maxPurchaseQuantity;
if (maxPurchase != null && maxPurchase > 0) {
const productVariantIds = variant.product.variants.getItems().map((v) => v.id);
const totalProductQuantityInCart =
cart?.items?.reduce(
(sum, item) => (productVariantIds.includes(item.variantId) ? sum + item.quantity : sum),
0,
) ?? 0;
const newTotalProductQuantity = totalProductQuantityInCart - existingQuantityInCart + quantityToAdd;
if (newTotalProductQuantity > maxPurchase) {
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
}
}
return variant;
}
+15 -1
View File
@@ -361,9 +361,12 @@ export class OrdersService {
private async buildOrderItemsData(cart: Cart, shopId: string): Promise<OrderItemData[]> {
const orderItemsData: OrderItemData[] = [];
const productQuantityMap = new Map<string, number>();
for (const cartItem of cart.items) {
const variant = await this.em.findOne(Variant, { id: cartItem.variantId }, { populate: ['product', 'product.shop'] });
const variant = await this.em.findOne(Variant, { id: cartItem.variantId }, {
populate: ['product', 'product.shop', 'product.variants'],
});
if (!variant) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND);
if (variant.product.shop.id !== shopId) {
@@ -374,6 +377,17 @@ export class OrdersService {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
}
const maxPurchase = variant.product.maxPurchaseQuantity;
if (maxPurchase != null && maxPurchase > 0) {
const productId = variant.product.id;
const currentProductTotal = productQuantityMap.get(productId) ?? 0;
const newProductTotal = currentProductTotal + cartItem.quantity;
if (newProductTotal > maxPurchase) {
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
}
productQuantityMap.set(productId, newProductTotal);
}
orderItemsData.push({
variant,
quantity: cartItem.quantity,
@@ -118,4 +118,10 @@ export class CreateProductDto {
@ApiPropertyOptional({ example: 1 })
order?: number;
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
@ApiPropertyOptional({ description: 'Maximum quantity per order; omit or null for unlimited', example: 5 })
maxPurchaseQuantity?: number | null;
}
@@ -56,4 +56,8 @@ export class Product extends BaseEntity {
@Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
price?: number;
/** Maximum quantity per order; null means unlimited */
@Property({ type: 'int', nullable: true, default: null })
maxPurchaseQuantity: number | null = null;
}
@@ -43,6 +43,7 @@ export class ProductService {
discount: rest.discount ?? 0,
isSpecialOffer: rest.isSpecialOffer ?? false,
order: rest.order ?? null,
maxPurchaseQuantity: rest.maxPurchaseQuantity ?? null,
// map single-title/content DTO to localized fields
title: rest.title,
attribute: rest.attribute,
@@ -240,10 +241,12 @@ export class ProductService {
}
async findOrFailVariant(variantId: string) {
const variant = await this.em.findOne(Variant, { id: variantId }, { populate: ['product', 'product.shop',] });
const variant = await this.em.findOne(Variant, { id: variantId }, {
populate: ['product', 'product.shop', 'product.variants'],
});
if (!variant) throw new NotFoundException('Variant not found');
return variant
return variant;
}
}