refactor cart

This commit is contained in:
2025-12-15 23:07:50 +03:30
parent fea123f61e
commit 3e4a4eebe4
13 changed files with 494 additions and 414 deletions
+36 -127
View File
@@ -1,18 +1,27 @@
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
import { CartService } from '../providers/cart.service';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { UpdateItemQuantityDto } from '../dto/update-item.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
import { SetAddressDto } from '../dto/set-address.dto';
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
import { SetDescriptionDto } from '../dto/set-description.dto';
import { SetTableNumberDto } from '../dto/set-table-number.dto';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
import { AuthGuard } from '../../auth/guards/auth.guard';
import { UserId } from 'src/common/decorators/user-id.decorator';
import { RestId } from 'src/common/decorators/rest-id.decorator';
const API_HEADER_SLUG = {
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
};
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiTags('cart')
@@ -22,43 +31,22 @@ export class CartController {
@Get()
@ApiOperation({ summary: 'Get cart for current user and restaurant' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
async findOne(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.getOrCreateCart(userId, restaurantId);
}
@Post('items/:foodId/increment')
@ApiOperation({ summary: 'Increment item quantity in cart by 1' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.incrementItem(userId, restaurantId, foodId);
return this.cartService.incrementItem(userId, restaurantId, foodId, 1);
}
@Post('items/:foodId/decrement')
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' })
async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.decrementItem(userId, restaurantId, foodId);
@@ -66,14 +54,7 @@ export class CartController {
@Post('items/bulk')
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: BulkAddItemsToCartDto })
bulkAddItems(
@UserId() userId: string,
@@ -83,37 +64,9 @@ export class CartController {
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
}
@Patch('items/:foodId')
@ApiOperation({ summary: 'Update item quantity in cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
@ApiBody({ type: UpdateItemQuantityDto })
async updateItemQuantity(
@UserId() userId: string,
@RestId() restaurantId: string,
@Param('foodId') foodId: string,
@Body() updateItemDto: UpdateItemQuantityDto,
) {
return this.cartService.updateItemQuantity(userId, restaurantId, foodId, updateItemDto);
}
@Delete('items/:foodId')
@ApiOperation({ summary: 'Remove item from cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.removeItem(userId, restaurantId, foodId);
@@ -121,28 +74,14 @@ export class CartController {
@Delete()
@ApiOperation({ summary: 'Clear entire cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.clearCart(userId, restaurantId);
}
@Post('apply-coupon')
@ApiOperation({ summary: 'Apply coupon to cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: ApplyCouponDto })
async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
@@ -150,28 +89,14 @@ export class CartController {
@Delete('coupon')
@ApiOperation({ summary: 'Remove coupon from cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.removeCoupon(userId, restaurantId);
}
@Patch('address')
@ApiOperation({ summary: 'Set address for cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetAddressDto })
async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) {
return this.cartService.setAddress(userId, restaurantId, setAddressDto);
@@ -179,14 +104,7 @@ export class CartController {
@Patch('payment-method')
@ApiOperation({ summary: 'Set payment method for cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetPaymentMethodDto })
async setPaymentMethod(
@UserId() userId: string,
@@ -198,14 +116,7 @@ export class CartController {
@Patch('delivery-method')
@ApiOperation({ summary: 'Set delivery method for cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetDeliveryMethodDto })
setDeliveryMethod(
@UserId() userId: string,
@@ -217,14 +128,7 @@ export class CartController {
@Patch('description')
@ApiOperation({ summary: 'Set description for cart' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetDescriptionDto })
setDescription(
@UserId() userId: string,
@@ -236,14 +140,7 @@ export class CartController {
@Patch('table-number')
@ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' })
@ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetTableNumberDto })
setTableNumber(
@UserId() userId: string,
@@ -252,4 +149,16 @@ export class CartController {
) {
return this.cartService.setTableNumber(userId, restaurantId, setTableNumberDto);
}
@Patch('car-delivery')
@ApiOperation({ summary: 'Set car delivery for cart' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetCarDeliveryDto })
setCarDelivery(
@UserId() userId: string,
@RestId() restaurantId: string,
@Body() setCarDeliveryDto: SetCarDeliveryDto,
) {
return this.cartService.setCarDelivery(userId, restaurantId, setCarDeliveryDto);
}
}
-15
View File
@@ -1,15 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
export class AddItemToCartDto {
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
@IsNotEmpty()
@IsNumber()
@Min(1)
quantity!: number;
@ApiProperty({ description: 'Food ID' })
@IsNotEmpty()
@IsString()
foodId!: string;
}
+14 -2
View File
@@ -1,7 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsNumber, Min, IsString } from 'class-validator';
import { Type } from 'class-transformer';
import { AddItemToCartDto } from './add-item.dto';
class AddItemToCartDto {
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
@IsNotEmpty()
@IsNumber()
@Min(1)
quantity!: number;
@ApiProperty({ description: 'Food ID' })
@IsNotEmpty()
@IsString()
foodId!: string;
}
export class BulkAddItemsToCartDto {
@ApiProperty({
@@ -0,0 +1,41 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumber, IsLatitude, IsLongitude } from 'class-validator';
export class SetCarDeliveryDto {
@ApiProperty({ description: 'Car model', example: 'Toyota Camry' })
@IsNotEmpty()
@IsString()
carModel!: string;
@ApiProperty({ description: 'Car color', example: 'White' })
@IsNotEmpty()
@IsString()
carColor!: string;
@ApiProperty({ description: 'License plate number', example: '12ABC345' })
@IsNotEmpty()
@IsString()
plateNumber!: string;
@ApiProperty({ description: 'Delivery address', example: '123 Main Street, City' })
@IsNotEmpty()
@IsString()
address!: string;
@ApiProperty({ description: 'Latitude coordinate', example: 35.6892 })
@IsNotEmpty()
@IsNumber()
@IsLatitude()
latitude!: number;
@ApiProperty({ description: 'Longitude coordinate', example: 51.389 })
@IsNotEmpty()
@IsNumber()
@IsLongitude()
longitude!: number;
@ApiProperty({ description: 'Full name of the recipient', example: 'John Doe' })
@IsNotEmpty()
@IsString()
fullName!: string;
}
-10
View File
@@ -1,10 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, Min } from 'class-validator';
export class UpdateItemQuantityDto {
@ApiProperty({ description: 'Quantity of the food item', example: 2, minimum: 1 })
@IsNotEmpty()
@IsNumber()
@Min(1)
quantity!: number;
}
+22 -1
View File
@@ -16,7 +16,6 @@ export interface Cart {
items: CartItem[];
coupon?: OrderCouponDetail;
addressId?: string;
paymentMethodId?: string;
deliveryMethodId?: string;
description?: string;
@@ -30,6 +29,28 @@ export interface Cart {
totalDiscount: number;
total: number;
carAddress?: {
carModel: string;
carColor: string;
plateNumber: string;
address?: string;
latitude?: number;
longitude?: number;
fullName: string;
phone: string;
} | null;
userAddress?: {
address?: string;
latitude?: number;
longitude?: number;
city: string;
province: string;
postalCode: string;
fullName: string;
phone: string;
} | null;
totalItems: number;
createdAt: string;
updatedAt: string;
+155 -210
View File
@@ -3,15 +3,14 @@ import { Food } from '../../foods/entities/food.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { EntityManager } from '@mikro-orm/postgresql';
import { CacheService } from '../../utils/cache.service';
import { AddItemToCartDto } from '../dto/add-item.dto';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { UpdateItemQuantityDto } from '../dto/update-item.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
import { SetAddressDto } from '../dto/set-address.dto';
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
import { SetDescriptionDto } from '../dto/set-description.dto';
import { SetTableNumberDto } from '../dto/set-table-number.dto';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
import { UserAddress } from '../../users/entities/user-address.entity';
import { User } from '../../users/entities/user.entity';
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
@@ -82,92 +81,35 @@ export class CartService {
return cart;
}
async findOne2(userId: string, restaurantId: string): Promise<Cart | null> {
const cart = await this.getCartByRestaurant(userId, restaurantId);
return cart;
}
/**
* Add item to cart (increment quantity if item exists, otherwise create new)
* Increment item quantity in cart by 1
*/
async addItem(userId: string, restaurantId: string, foodId: string, addItemDto: AddItemToCartDto): Promise<Cart> {
async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
const food = await this.validateAndGetFood(foodId, restaurantId, quantity);
// Find food
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) {
throw new NotFoundException(`Food with ID ${foodId} not found`);
}
// Check if food belongs to the restaurant
if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
}
// Check stock availability
const availableStock = food.inventory?.availableStock ?? 0;
if (availableStock < addItemDto.quantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
// Check if item already exists in cart
const existingItemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (existingItemIndex >= 0) {
// Update existing item quantity
const existingItem = cart.items[existingItemIndex];
const newQuantity = existingItem.quantity + addItemDto.quantity;
// Check stock for new total quantity
if (availableStock < newQuantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * newQuantity;
const itemTotalDiscount = itemDiscount * newQuantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
const newQuantity = existingItem.quantity + quantity;
this.validateStock(food, newQuantity);
cart.items[existingItemIndex] = {
...existingItem,
quantity: newQuantity,
totalPrice: itemFinalPrice,
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
};
} else {
// Create new item
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * addItemDto.quantity;
const itemTotalDiscount = itemDiscount * addItemDto.quantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
const cartItem: CartItem = {
foodId: food.id,
foodTitle: food.title,
quantity: addItemDto.quantity,
price: itemPrice,
discount: itemDiscount,
totalPrice: itemFinalPrice,
};
cart.items.push(cartItem);
cart.items.push(this.createCartItem(food, quantity));
}
// Recalculate cart totals
await this.recalculateCartTotals(cart);
await this.saveCart(cart);
return cart;
}
/**
* Increment item quantity in cart by 1
*/
async incrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
return this.addItem(userId, restaurantId, foodId, { foodId, quantity: 1 });
}
/**
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
*/
@@ -186,22 +128,15 @@ export class CartService {
// Remove item if quantity becomes 0 or less
cart.items.splice(itemIndex, 1);
} else {
// Update item quantity and recalculate price
const food = await this.em.findOne(Food, { id: foodId });
if (!food) {
throw new NotFoundException(`Food with ID ${foodId} not found`);
}
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * newQuantity;
const itemTotalDiscount = itemDiscount * newQuantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
cart.items[itemIndex] = {
...existingItem,
quantity: newQuantity,
totalPrice: itemFinalPrice,
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
};
}
@@ -218,121 +153,25 @@ export class CartService {
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
// Process each item
for (const addItemDto of bulkAddItemsDto.items) {
// Find food
const food = await this.em.findOne(Food, { id: addItemDto.foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) {
throw new NotFoundException(`Food with ID ${addItemDto.foodId} not found`);
}
// Check if food belongs to the restaurant
if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
}
// Check stock availability
const availableStock = food.inventory?.availableStock ?? 0;
if (availableStock < addItemDto.quantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
// Check if item already exists in cart
const food = await this.validateAndGetFood(addItemDto.foodId, restaurantId, addItemDto.quantity);
const existingItemIndex = cart.items.findIndex(item => item.foodId === addItemDto.foodId);
if (existingItemIndex >= 0) {
// Update existing item quantity
const existingItem = cart.items[existingItemIndex];
const newQuantity = existingItem.quantity + addItemDto.quantity;
// Check stock for new total quantity
if (availableStock < newQuantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * newQuantity;
const itemTotalDiscount = itemDiscount * newQuantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
this.validateStock(food, newQuantity);
cart.items[existingItemIndex] = {
...existingItem,
quantity: newQuantity,
totalPrice: itemFinalPrice,
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
};
} else {
// Create new item
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * addItemDto.quantity;
const itemTotalDiscount = itemDiscount * addItemDto.quantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
const cartItem: CartItem = {
foodId: food.id,
foodTitle: food.title,
quantity: addItemDto.quantity,
price: itemPrice,
discount: itemDiscount,
totalPrice: itemFinalPrice,
};
cart.items.push(cartItem);
cart.items.push(this.createCartItem(food, addItemDto.quantity));
}
}
// Recalculate cart totals once after all items are added
await this.recalculateCartTotals(cart);
await this.saveCart(cart);
return cart;
}
/**
* Update item quantity in cart
*/
async updateItemQuantity(
userId: string,
restaurantId: string,
foodId: string,
updateItemDto: UpdateItemQuantityDto,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) {
throw new NotFoundException(`Item with food ID ${foodId} not found in cart`);
}
const item = cart.items[itemIndex];
// Find food to check stock
const food = await this.em.findOne(Food, { id: item.foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) {
throw new NotFoundException(`Food with ID ${item.foodId} not found`);
}
// Check stock availability
const availableStock = food.inventory?.availableStock ?? 0;
if (availableStock < updateItemDto.quantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
// Update quantity
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * updateItemDto.quantity;
const itemTotalDiscount = itemDiscount * updateItemDto.quantity;
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
cart.items[itemIndex] = {
...item,
quantity: updateItemDto.quantity,
totalPrice: itemFinalPrice,
};
// Recalculate cart totals
await this.recalculateCartTotals(cart);
await this.saveCart(cart);
@@ -366,18 +205,17 @@ export class CartService {
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
// Calculate current order amount (subtotal after item discounts) for coupon validation
// const currentSubTotal = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// const currentItemsDiscount = cart.items.reduce((sum, item) => sum + item.discount * item.quantity, 0);
// const orderAmount = currentSubTotal - currentItemsDiscount;
const orderAmount = cart.subTotal - cart.itemsDiscount;
const user = await this.em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
// check if coupon is valid and belong to the restaurant
const { valid, coupon, message } = await this.couponService.validateCoupon(
applyCouponDto.code,
restaurantId,
Number(orderAmount),
user.phone,
);
if (!valid) {
@@ -388,20 +226,6 @@ export class CartService {
throw new BadRequestException('Coupon not found');
}
// Validate user phone if coupon is restricted to a specific user
if (coupon.userPhone) {
const user = await this.em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException('User not found');
}
if (user.phone !== coupon.userPhone) {
throw new BadRequestException(
'This coupon is only available for a specific user and cannot be applied to your account.',
);
}
}
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
if (coupon.maxUsesPerUser) {
// Use native query for JSONB field access with proper parameter binding
@@ -510,7 +334,14 @@ export class CartService {
throw new BadRequestException('Address does not belong to the current user');
}
cart.addressId = setAddressDto.addressId;
cart.userAddress = {
address: address.address,
city: address.city,
province: address.province || '',
postalCode: address.postalCode || '',
fullName: address.user.firstName + ' ' + address.user.lastName || '',
phone: address.user.phone,
};
cart.updatedAt = new Date().toISOString();
await this.saveCart(cart);
@@ -618,18 +449,20 @@ export class CartService {
async setTableNumber(userId: string, restaurantId: string, setTableNumberDto: SetTableNumberDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
// If delivery method is set, validate that table number is required for DineIn
if (cart.deliveryMethodId) {
const deliveryMethod = await this.em.findOne(Delivery, {
id: cart.deliveryMethodId,
restaurant: { id: restaurantId },
});
if (deliveryMethod && deliveryMethod.method === DeliveryMethodEnum.DineIn) {
if (!setTableNumberDto.tableNumber || setTableNumberDto.tableNumber.trim() === '') {
throw new BadRequestException('Table number is required when delivery method is DineIn');
}
}
if (!cart.deliveryMethodId) {
throw new BadRequestException('Delivery method must be set before setting table number');
}
const deliveryMethod = await this.em.findOne(Delivery, {
id: cart.deliveryMethodId,
restaurant: { id: restaurantId },
});
if (!deliveryMethod) {
throw new NotFoundException(
`Delivery method with ID ${cart.deliveryMethodId} not found for restaurant ${restaurantId}`,
);
}
if (deliveryMethod.method !== DeliveryMethodEnum.DineIn) {
throw new BadRequestException('Delivery method must be DineIn');
}
cart.tableNumber = setTableNumberDto.tableNumber;
@@ -640,6 +473,49 @@ export class CartService {
return cart;
}
/**
* Set car delivery for cart
*/
async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
if (!cart.deliveryMethodId)
throw new BadRequestException('Delivery method must be set before setting car delivery');
const deliveryMethod = await this.em.findOne(Delivery, {
id: cart.deliveryMethodId,
restaurant: { id: restaurantId },
});
if (!deliveryMethod) {
throw new NotFoundException(
`Delivery method with ID ${cart.deliveryMethodId} not found for restaurant ${restaurantId}`,
);
}
if (deliveryMethod.method !== DeliveryMethodEnum.DeliveryCar) {
throw new BadRequestException('Delivery method must be DeliveryCar');
}
const user = await this.em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
cart.carAddress = {
carModel: setCarDeliveryDto.carModel,
carColor: setCarDeliveryDto.carColor,
plateNumber: setCarDeliveryDto.plateNumber,
address: setCarDeliveryDto.address,
latitude: setCarDeliveryDto.latitude,
longitude: setCarDeliveryDto.longitude,
fullName: setCarDeliveryDto.fullName,
phone: user.phone,
};
cart.updatedAt = new Date().toISOString();
await this.saveCart(cart);
return cart;
}
/**
* Recalculate cart totals (including coupon discount and tax)
*/
@@ -778,7 +654,7 @@ export class CartService {
* Get cart by restaurant for a user
*/
private async getCartByRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
const cacheKey = this.getCacheKey(userId, restaurantId);
const cachedCart = await this.cacheService.get<string>(cacheKey);
if (cachedCart) {
@@ -799,12 +675,81 @@ export class CartService {
* Save cart to cache
*/
private async saveCart(cart: Cart): Promise<void> {
const cacheKey = `${this.CART_KEY_PREFIX}:${cart.userId}:${cart.restaurantId}`;
const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId);
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
}
/**
* Clear cart from cache
*/
async clearCart(userId: string, restaurantId: string): Promise<void> {
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
const cacheKey = this.getCacheKey(userId, restaurantId);
return this.cacheService.del(cacheKey);
}
/**
* Generate cache key for cart
*/
private getCacheKey(userId: string, restaurantId: string): string {
return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
}
/**
* Validate food exists, belongs to restaurant, has inventory, and check stock
*/
private async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) {
throw new NotFoundException(`Food with ID ${foodId} not found`);
}
if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
}
if (!food.inventory) {
throw new BadRequestException(`The food ${food.title || food.id} does not have inventory`);
}
this.validateStock(food, quantity);
return food;
}
/**
* Validate stock availability for food
*/
private validateStock(food: Food, quantity: number): void {
const availableStock = food.inventory!.availableStock;
if (availableStock < quantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
}
}
/**
* Calculate total price for a cart item
*/
private calculateItemTotalPrice(food: Food, quantity: number): number {
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
const itemTotalPrice = itemPrice * quantity;
const itemTotalDiscount = itemDiscount * quantity;
return itemTotalPrice - itemTotalDiscount;
}
/**
* Create a new cart item from food
*/
private createCartItem(food: Food, quantity: number): CartItem {
const itemPrice = food.price || 0;
const itemDiscount = food.discount || 0;
return {
foodId: food.id,
foodTitle: food.title,
quantity,
price: itemPrice,
discount: itemDiscount,
totalPrice: this.calculateItemTotalPrice(food, quantity),
};
}
}