cart and coupon

This commit is contained in:
2025-12-06 20:26:37 +03:30
parent a7d28ab882
commit e342c00e25
9 changed files with 114 additions and 122 deletions
+83 -33
View File
@@ -13,7 +13,10 @@ import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
import { UserAddress } from '../../users/entities/user-address.entity';
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { Cart, CartItem, CartCoupon } from '../interfaces/cart.interface';
import { Cart, CartItem } from '../interfaces/cart.interface';
import { CouponService } from 'src/modules/coupons/providers/coupon.service';
import { OrderCouponDetail } from 'src/modules/orders/interface/order-status';
import { CouponType } from 'src/modules/coupons/entities/coupon.entity';
@Injectable()
export class CartService {
@@ -23,6 +26,7 @@ export class CartService {
constructor(
private readonly em: EntityManager,
private readonly cacheService: CacheService,
private readonly couponService: CouponService,
) {}
/**
@@ -47,7 +51,7 @@ export class CartService {
restaurantId,
restaurantName: restaurant.name,
items: [],
shipmentFee: 0,
deliveryFee: 0,
subTotal: 0,
itemsDiscount: 0,
couponDiscount: 0,
@@ -66,13 +70,14 @@ export class CartService {
/**
* Find cart by user and restaurant
*/
async findOne(userId: string, restaurantId: string): Promise<Cart> {
async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.getCartByRestaurant(userId, restaurantId);
if (!cart) {
throw new NotFoundException('Cart not found');
}
return cart;
}
async findOne2(userId: string, restaurantId: string): Promise<Cart | null> {
const cart = await this.getCartByRestaurant(userId, restaurantId);
return cart;
@@ -162,7 +167,7 @@ export class CartService {
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
*/
async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) {
@@ -287,7 +292,7 @@ export class CartService {
foodId: string,
updateItemDto: UpdateItemQuantityDto,
): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) {
@@ -331,7 +336,7 @@ export class CartService {
* Remove item from cart
*/
async removeItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) {
@@ -352,20 +357,60 @@ export class CartService {
* Apply coupon to cart
*/
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
// TODO: Implement actual coupon validation logic
// For now, this is a placeholder implementation
// You should validate the coupon code against your coupon/discount system
// 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;
// Example coupon structure (replace with your actual coupon validation)
const coupon: CartCoupon = {
code: applyCouponDto.code,
discount: 10, // Example: 10% discount
discountType: 'percentage', // or 'amount'
const orderAmount = cart.subTotal - cart.itemsDiscount;
const { valid, coupon } = await this.couponService.validateCoupon(
applyCouponDto.code,
restaurantId,
Number(orderAmount),
);
if (!valid) {
throw new BadRequestException('Invalid coupon code');
}
if (!coupon) {
throw new BadRequestException('Coupon not found');
}
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
if (coupon.maxUsesPerUser) {
const result = await this.em.getConnection().execute<{ count: string }>(
`SELECT COUNT(*) as count
FROM orders
WHERE user_id = $1
AND coupon_detail IS NOT NULL
AND coupon_detail->>'couponId' = $2
AND deleted_at IS NULL`,
[userId, coupon.id],
);
const userCouponUsageCount = parseInt((result[0] as { count: string })?.count || '0', 10);
if (userCouponUsageCount >= coupon.maxUsesPerUser) {
throw new BadRequestException(
`You have reached the maximum number of uses (${coupon.maxUsesPerUser}) for this coupon`,
);
}
}
const couponDetail: OrderCouponDetail = {
couponId: coupon.id,
couponName: coupon.name,
couponCode: coupon.code,
type: coupon.type,
value: coupon.value,
maxDiscount: coupon.maxDiscount,
};
cart.coupon = coupon;
cart.coupon = couponDetail;
// Recalculate cart totals
await this.recalculateCartTotals(cart);
@@ -378,7 +423,7 @@ export class CartService {
* Remove coupon from cart
*/
async removeCoupon(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
cart.coupon = undefined;
@@ -393,7 +438,7 @@ export class CartService {
* Set address for cart
*/
async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
// Find and validate address belongs to user
const address = await this.em.findOne(UserAddress, { id: setAddressDto.addressId }, { populate: ['user'] });
@@ -422,7 +467,7 @@ export class CartService {
restaurantId: string,
setPaymentMethodDto: SetPaymentMethodDto,
): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
// Find and validate payment method belongs to restaurant
const paymentMethod = await this.em.findOne(
@@ -462,7 +507,7 @@ export class CartService {
restaurantId: string,
setDeliveryMethodDto: SetDeliveryMethodDto,
): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId);
const cart = await this.findOneOrFail(userId, restaurantId);
// Find and validate delivery method belongs to restaurant
const deliveryMethod = await this.em.findOne(
@@ -487,7 +532,7 @@ export class CartService {
cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId;
// Recalculate cart totals (this will update shipmentFee based on delivery method)
// Recalculate cart totals (this will update deliveryFee based on delivery method)
await this.recalculateCartTotals(cart);
await this.saveCart(cart);
@@ -515,14 +560,19 @@ export class CartService {
// Calculate coupon discount
let couponDiscount = 0;
if (cart.coupon) {
if (cart.coupon.discountType === 'percentage') {
const priceAfterItemDiscount = subTotal - itemsDiscount;
couponDiscount = (priceAfterItemDiscount * cart.coupon.discount) / 100;
const priceAfterItemDiscount = subTotal - itemsDiscount;
if (cart.coupon.type === CouponType.PERCENTAGE) {
let discount = (priceAfterItemDiscount * cart.coupon.value) / 100;
// Apply maxDiscount limit if set
if (cart.coupon.maxDiscount && discount > cart.coupon.maxDiscount) {
discount = cart.coupon.maxDiscount;
}
couponDiscount = discount;
} else {
// amount discount
const priceAfterItemDiscount = subTotal - itemsDiscount;
couponDiscount = Math.min(cart.coupon.discount, priceAfterItemDiscount);
// FIXED amount discount
couponDiscount = Math.min(cart.coupon.value, priceAfterItemDiscount);
}
}
@@ -542,17 +592,17 @@ export class CartService {
cart.tax = tax;
// Shipment fee is calculated separately via delivery method
let shipmentFee = 0;
let deliveryFee = 0;
if (cart.deliveryMethodId) {
const deliveryMethod = await this.em.findOne(Delivery, { id: cart.deliveryMethodId });
if (deliveryMethod && deliveryMethod.enabled) {
shipmentFee = Number(deliveryMethod.deliveryFee) || 0;
deliveryFee = Number(deliveryMethod.deliveryFee) || 0;
}
}
cart.shipmentFee = shipmentFee;
cart.deliveryFee = deliveryFee;
// Calculate total: total = subtotal totalDiscount + tax + shipmentFee
cart.total = subTotal - cart.totalDiscount + tax + cart.shipmentFee;
// Calculate total: total = subtotal totalDiscount + tax + deliveryFee
cart.total = subTotal - cart.totalDiscount + tax + cart.deliveryFee;
cart.totalItems = totalItems;
cart.updatedAt = new Date().toISOString();
}
@@ -574,7 +624,7 @@ export class CartService {
typeof cart.couponDiscount === 'number' &&
typeof cart.totalDiscount === 'number' &&
typeof cart.tax === 'number' &&
typeof cart.shipmentFee === 'number' &&
typeof cart.deliveryFee === 'number' &&
typeof cart.total === 'number' &&
typeof cart.totalItems === 'number' &&
typeof cart.createdAt === 'string' &&