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_NOT_BELONGS_TO_SHOP = 'محصول به این فروشگاه تعلق ندارد',
PRODUCT_NO_INVENTORY = 'محصول موجودی ندارد', PRODUCT_NO_INVENTORY = 'محصول موجودی ندارد',
INSUFFICIENT_STOCK = 'موجودی کافی نیست', INSUFFICIENT_STOCK = 'موجودی کافی نیست',
MAX_PURCHASE_EXCEEDED = 'تعداد درخواستی بیش از حداکثر مجاز خرید این محصول است',
USER_NOT_FOUND = 'کاربر یافت نشد', USER_NOT_FOUND = 'کاربر یافت نشد',
ADDRESS_NOT_FOUND = 'آدرس یافت نشد', ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
SHOP_NOT_FOUND = 'فروشگاه یافت نشد', SHOP_NOT_FOUND = 'فروشگاه یافت نشد',
@@ -68,6 +68,7 @@ export class CartItemService {
shopId, shopId,
quantityToAdd, quantityToAdd,
existingQuantityInCart, existingQuantityInCart,
cart,
); );
if (index < 0) { 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 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( async validateAndGetVariant(
variantId: string, variantId: string,
shopId: string, shopId: string,
quantityToAdd: number, quantityToAdd: number,
existingQuantityInCart: number = 0, existingQuantityInCart: number = 0,
cart?: { items: { variantId: string; quantity: number }[] },
): Promise<Variant> { ): Promise<Variant> {
const variant = await this.productService.findOrFailVariant(variantId); 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; return variant;
} }
+15 -1
View File
@@ -361,9 +361,12 @@ export class OrdersService {
private async buildOrderItemsData(cart: Cart, shopId: string): Promise<OrderItemData[]> { private async buildOrderItemsData(cart: Cart, shopId: string): Promise<OrderItemData[]> {
const orderItemsData: OrderItemData[] = []; const orderItemsData: OrderItemData[] = [];
const productQuantityMap = new Map<string, number>();
for (const cartItem of cart.items) { 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) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND);
if (variant.product.shop.id !== shopId) { if (variant.product.shop.id !== shopId) {
@@ -374,6 +377,17 @@ export class OrdersService {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK); 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({ orderItemsData.push({
variant, variant,
quantity: cartItem.quantity, quantity: cartItem.quantity,
@@ -118,4 +118,10 @@ export class CreateProductDto {
@ApiPropertyOptional({ example: 1 }) @ApiPropertyOptional({ example: 1 })
order?: number; 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 }) @Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
price?: number; 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, discount: rest.discount ?? 0,
isSpecialOffer: rest.isSpecialOffer ?? false, isSpecialOffer: rest.isSpecialOffer ?? false,
order: rest.order ?? null, order: rest.order ?? null,
maxPurchaseQuantity: rest.maxPurchaseQuantity ?? null,
// map single-title/content DTO to localized fields // map single-title/content DTO to localized fields
title: rest.title, title: rest.title,
attribute: rest.attribute, attribute: rest.attribute,
@@ -240,10 +241,12 @@ export class ProductService {
} }
async findOrFailVariant(variantId: string) { 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'); if (!variant) throw new NotFoundException('Variant not found');
return variant return variant;
} }
} }