add max purchase count
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user