From eb4d19ab81123b216f96a1d6b9c570783dee7ede Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 14 Feb 2026 15:52:43 +0330 Subject: [PATCH] add max purchase count --- src/common/enums/message.enum.ts | 1 + .../cart/providers/cart-item.service.ts | 1 + .../cart/providers/cart-validation.service.ts | 18 +++++++++++++++++- src/modules/orders/providers/orders.service.ts | 16 +++++++++++++++- src/modules/products/dto/create-product.dto.ts | 6 ++++++ .../products/entities/product.entity.ts | 4 ++++ .../products/providers/product.service.ts | 7 +++++-- 7 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 0f78b5d..7314cca 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -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 = 'فروشگاه یافت نشد', diff --git a/src/modules/cart/providers/cart-item.service.ts b/src/modules/cart/providers/cart-item.service.ts index 0e36157..b470eee 100644 --- a/src/modules/cart/providers/cart-item.service.ts +++ b/src/modules/cart/providers/cart-item.service.ts @@ -68,6 +68,7 @@ export class CartItemService { shopId, quantityToAdd, existingQuantityInCart, + cart, ); if (index < 0) { diff --git a/src/modules/cart/providers/cart-validation.service.ts b/src/modules/cart/providers/cart-validation.service.ts index 8a3c273..8653f3f 100644 --- a/src/modules/cart/providers/cart-validation.service.ts +++ b/src/modules/cart/providers/cart-validation.service.ts @@ -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 { 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; } diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index 0c4df99..d9f62b1 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -361,9 +361,12 @@ export class OrdersService { private async buildOrderItemsData(cart: Cart, shopId: string): Promise { const orderItemsData: OrderItemData[] = []; + const productQuantityMap = new Map(); 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, diff --git a/src/modules/products/dto/create-product.dto.ts b/src/modules/products/dto/create-product.dto.ts index c80c476..cb58653 100644 --- a/src/modules/products/dto/create-product.dto.ts +++ b/src/modules/products/dto/create-product.dto.ts @@ -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; } diff --git a/src/modules/products/entities/product.entity.ts b/src/modules/products/entities/product.entity.ts index 7e551e6..d73a75f 100644 --- a/src/modules/products/entities/product.entity.ts +++ b/src/modules/products/entities/product.entity.ts @@ -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; } diff --git a/src/modules/products/providers/product.service.ts b/src/modules/products/providers/product.service.ts index d1c5197..3b1acf1 100644 --- a/src/modules/products/providers/product.service.ts +++ b/src/modules/products/providers/product.service.ts @@ -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; } }