cart
This commit is contained in:
@@ -13,7 +13,7 @@ class AddItemToCartDto {
|
|||||||
@ApiProperty({ description: 'Product ID' })
|
@ApiProperty({ description: 'Product ID' })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
productId!: string;
|
variantId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BulkAddItemsToCartDto {
|
export class BulkAddItemsToCartDto {
|
||||||
@@ -21,8 +21,8 @@ export class BulkAddItemsToCartDto {
|
|||||||
description: 'Array of items to add to cart',
|
description: 'Array of items to add to cart',
|
||||||
type: [AddItemToCartDto],
|
type: [AddItemToCartDto],
|
||||||
example: [
|
example: [
|
||||||
{ productId: 'product-123', quantity: 2 },
|
{ variantId: 'product-123', quantity: 2 },
|
||||||
{ productId: 'product-456', quantity: 1 },
|
{ productId: 'product-456', quantity: 1 },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class CartItemDto {
|
|||||||
@ApiProperty({ description: 'Product ID' })
|
@ApiProperty({ description: 'Product ID' })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
foodId!: string;
|
variantId!: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Quantity of the product item', example: 2, minimum: 1 })
|
@ApiProperty({ description: 'Quantity of the product item', example: 2, minimum: 1 })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { OrderCouponDetail, OrderUserAddress } from 'src/modules/orders/interface/order.interface';
|
import type { OrderCouponDetail, OrderUserAddress } from 'src/modules/orders/interface/order.interface';
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
productId: string;
|
variantId: string;
|
||||||
productTitle?: string;
|
productTitle?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class CartCalculationService {
|
|||||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||||
|
|
||||||
for (const item of cart.items) {
|
for (const item of cart.items) {
|
||||||
const product = foodMap.get(item.productId);
|
const product = foodMap.get(item.variantId);
|
||||||
if (!product) continue;
|
if (!product) continue;
|
||||||
|
|
||||||
const matchesCategory =
|
const matchesCategory =
|
||||||
@@ -227,10 +227,10 @@ export class CartCalculationService {
|
|||||||
* Get products in cart with categories
|
* Get products in cart with categories
|
||||||
*/
|
*/
|
||||||
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
||||||
const foodIds = cart.items.map(item => item.productId);
|
const variantIds = cart.items.map(item => item.variantId);
|
||||||
if (foodIds.length === 0) return new Map();
|
if (variantIds.length === 0) return new Map();
|
||||||
|
|
||||||
const products = await this.em.find(Product, { id: { $in: foodIds } }, { populate: ['category'] });
|
const products = await this.em.find(Product, { id: { $in: variantIds } }, { populate: ['category'] });
|
||||||
return new Map(products.map(f => [f.id, f]));
|
return new Map(products.map(f => [f.id, f]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class CartItemService {
|
|||||||
const itemDiscount = product.discount || 0;
|
const itemDiscount = product.discount || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
productId: product.id,
|
variantId: product.id,
|
||||||
productTitle: product.title,
|
productTitle: product.title,
|
||||||
quantity,
|
quantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
@@ -39,7 +39,7 @@ export class CartItemService {
|
|||||||
const itemDiscount = product.discount || 0;
|
const itemDiscount = product.discount || 0;
|
||||||
return {
|
return {
|
||||||
...(previous ?? ({} as CartItem)),
|
...(previous ?? ({} as CartItem)),
|
||||||
productId: product.id,
|
variantId: product.id,
|
||||||
productTitle: product.title,
|
productTitle: product.title,
|
||||||
quantity,
|
quantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
@@ -51,8 +51,8 @@ export class CartItemService {
|
|||||||
/**
|
/**
|
||||||
* Get item index in cart
|
* Get item index in cart
|
||||||
*/
|
*/
|
||||||
getItemIndex(cart: Cart, productId: string): number {
|
getItemIndex(cart: Cart, variantId: string): number {
|
||||||
return cart.items.findIndex(item => item.productId === productId);
|
return cart.items.findIndex(item => item.variantId === variantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class CartValidationService {
|
|||||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||||
|
|
||||||
for (const item of cart.items) {
|
for (const item of cart.items) {
|
||||||
const product = foodMap.get(item.productId);
|
const product = foodMap.get(item.variantId);
|
||||||
if (!product) continue;
|
if (!product) continue;
|
||||||
|
|
||||||
const matchesCategory = hasCategoryRestriction && product.category && categoryRestriction.includes(product.category.id);
|
const matchesCategory = hasCategoryRestriction && product.category && categoryRestriction.includes(product.category.id);
|
||||||
@@ -190,7 +190,7 @@ export class CartValidationService {
|
|||||||
* Get products in cart with categories
|
* Get products in cart with categories
|
||||||
*/
|
*/
|
||||||
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
||||||
const productIds = cart.items.map(item => item.productId);
|
const productIds = cart.items.map(item => item.variantId);
|
||||||
if (productIds.length === 0) return new Map();
|
if (productIds.length === 0) return new Map();
|
||||||
|
|
||||||
const products = await this.em.find(Product, { id: { $in: productIds } }, { populate: ['category'] });
|
const products = await this.em.find(Product, { id: { $in: productIds } }, { populate: ['category'] });
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export class CartService {
|
|||||||
const cart = await this.getOrCreateCart(userId, shopId);
|
const cart = await this.getOrCreateCart(userId, shopId);
|
||||||
|
|
||||||
for (const addItemDto of bulkAddItemsDto.items) {
|
for (const addItemDto of bulkAddItemsDto.items) {
|
||||||
await this.itemService.addOrIncrementItem(cart, addItemDto.productId, shopId, addItemDto.quantity);
|
await this.itemService.addOrIncrementItem(cart, addItemDto.variantId, shopId, addItemDto.quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.recalculateAndSaveCart(cart);
|
return this.recalculateAndSaveCart(cart);
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ export class OrdersService {
|
|||||||
const orderItemsData: OrderItemData[] = [];
|
const orderItemsData: OrderItemData[] = [];
|
||||||
|
|
||||||
for (const cartItem of cart.items) {
|
for (const cartItem of cart.items) {
|
||||||
const variant = await this.em.findOne(Variant, { id: cartItem.productId }, { populate: ['product', 'product.shop'] });
|
const variant = await this.em.findOne(Variant, { id: cartItem.variantId }, { populate: ['product', 'product.shop'] });
|
||||||
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) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Max,
|
|
||||||
Min,
|
Min,
|
||||||
|
Length,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
@@ -21,9 +21,14 @@ export class CreateVariantDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty()
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1000)
|
||||||
|
@IsNotEmpty()
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
price: number
|
price: number
|
||||||
|
|
||||||
@@ -46,6 +51,7 @@ export class CreateProductDto {
|
|||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Length(1, 100)
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
type: CreateVariantDto,
|
type: CreateVariantDto,
|
||||||
example: [{
|
example: [{
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ export class ProductService {
|
|||||||
private readonly shopService: ShopService,
|
private readonly shopService: ShopService,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
) { }
|
) { }
|
||||||
|
// TODO : Messages must be in persian
|
||||||
async create(shopId: string, dto: CreateProductDto) {
|
async create(shopId: string, dto: CreateProductDto) {
|
||||||
const { categoryId, variants, ...rest } = dto;
|
const { categoryId, variants, ...rest } = dto;
|
||||||
const shop = await this.shopService.findOrFail(shopId);
|
const shop = await this.shopService.findOrFail(shopId);
|
||||||
|
|
||||||
const category = await this.categoryService.findOrFail(categoryId)
|
const category = await this.categoryService.findOrFail(categoryId)
|
||||||
|
|
||||||
|
if (variants.length > 1 && !dto.attribute) {
|
||||||
|
throw new BadRequestException('Attribute is required for multiple variants');
|
||||||
|
}
|
||||||
|
|
||||||
const product = await this.em.transactional(async em => {
|
const product = await this.em.transactional(async em => {
|
||||||
// prepare data with defaults for required fields so repository.create typing is satisfied
|
// prepare data with defaults for required fields so repository.create typing is satisfied
|
||||||
const data: RequiredEntityData<Product> = {
|
const data: RequiredEntityData<Product> = {
|
||||||
@@ -62,8 +66,6 @@ export class ProductService {
|
|||||||
return product;
|
return product;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,15 +98,13 @@ export class ProductService {
|
|||||||
|
|
||||||
|
|
||||||
async findByShop(slug: string): Promise<Product[]> {
|
async findByShop(slug: string): Promise<Product[]> {
|
||||||
const shop = await this.shopService.findOrFailBySlug(slug);
|
|
||||||
|
|
||||||
const queryFilter: FilterQuery<Product> = {
|
const queryFilter: FilterQuery<Product> = {
|
||||||
shop: { slug },
|
shop: { slug },
|
||||||
isActive: true,
|
isActive: true,
|
||||||
} as unknown as FilterQuery<Product>;
|
} as unknown as FilterQuery<Product>;
|
||||||
|
|
||||||
return this.productRepository.find(queryFilter, {
|
return this.productRepository.find(queryFilter, {
|
||||||
populate: ['category'],
|
populate: ['category', 'variants'],
|
||||||
orderBy: { order: 'asc' }
|
orderBy: { order: 'asc' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ export class ProductService {
|
|||||||
}
|
}
|
||||||
const userRef = this.em.getReference(User, userId);
|
const userRef = this.em.getReference(User, userId);
|
||||||
const productRef = this.em.getReference(Product, productId);
|
const productRef = this.em.getReference(Product, productId);
|
||||||
|
|
||||||
const newFavorite = this.em.create(Favorite, {
|
const newFavorite = this.em.create(Favorite, {
|
||||||
user: userRef,
|
user: userRef,
|
||||||
product: productRef,
|
product: productRef,
|
||||||
|
|||||||
Reference in New Issue
Block a user