refactor cart
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user