diff --git a/src/modules/cart/cart.module.ts b/src/modules/cart/cart.module.ts index c3fb424..9936574 100644 --- a/src/modules/cart/cart.module.ts +++ b/src/modules/cart/cart.module.ts @@ -9,6 +9,7 @@ import { UserAddress } from '../users/entities/user-address.entity'; import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; import { UtilsModule } from '../utils/utils.module'; +import { CouponModule } from '../coupons/coupon.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { UtilsModule } from '../utils/utils.module'; AuthModule, JwtModule, UtilsModule, + CouponModule, ], controllers: [CartController], providers: [CartService], diff --git a/src/modules/cart/controllers/cart.controller.ts b/src/modules/cart/controllers/cart.controller.ts index e245cbf..1546775 100644 --- a/src/modules/cart/controllers/cart.controller.ts +++ b/src/modules/cart/controllers/cart.controller.ts @@ -20,8 +20,6 @@ export class CartController { @Get() @ApiOperation({ summary: 'Get cart for current user and restaurant' }) - @ApiResponse({ status: 200, description: 'Cart retrieved successfully' }) - @ApiResponse({ status: 404, description: 'Cart not found' }) async findOne(@UserId() userId: string, @RestId() restaurantId: string) { return this.cartService.getOrCreateCart(userId, restaurantId); } @@ -29,9 +27,6 @@ export class CartController { @Post('items/:foodId/increment') @ApiOperation({ summary: 'Increment item quantity in cart by 1' }) @ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' }) - @ApiResponse({ status: 201, description: 'Item incremented in cart successfully' }) - @ApiResponse({ status: 400, description: 'Bad request (e.g., insufficient stock)' }) - @ApiResponse({ status: 404, description: 'Food not found' }) async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { return this.cartService.incrementItem(userId, restaurantId, foodId); } @@ -39,8 +34,6 @@ export class CartController { @Post('items/:foodId/decrement') @ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' }) @ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' }) - @ApiResponse({ status: 200, description: 'Item decremented in cart successfully' }) - @ApiResponse({ status: 404, description: 'Item not found in cart' }) async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { return this.cartService.decrementItem(userId, restaurantId, foodId); } @@ -48,9 +41,6 @@ export class CartController { @Post('items/bulk') @ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' }) @ApiBody({ type: BulkAddItemsToCartDto }) - @ApiResponse({ status: 201, description: 'Items added to cart successfully' }) - @ApiResponse({ status: 400, description: 'Bad request (e.g., insufficient stock, invalid items)' }) - @ApiResponse({ status: 404, description: 'Food not found' }) bulkAddItems( @UserId() userId: string, @RestId() restaurantId: string, @@ -63,9 +53,6 @@ export class CartController { @ApiOperation({ summary: 'Update item quantity in cart' }) @ApiParam({ name: 'foodId', description: 'Food ID in the cart' }) @ApiBody({ type: UpdateItemQuantityDto }) - @ApiResponse({ status: 200, description: 'Item quantity updated successfully' }) - @ApiResponse({ status: 400, description: 'Bad request (e.g., insufficient stock)' }) - @ApiResponse({ status: 404, description: 'Item not found in cart' }) async updateItemQuantity( @UserId() userId: string, @RestId() restaurantId: string, @@ -94,8 +81,6 @@ export class CartController { @Post('apply-coupon') @ApiOperation({ summary: 'Apply coupon to cart' }) @ApiBody({ type: ApplyCouponDto }) - @ApiResponse({ status: 200, description: 'Coupon applied successfully' }) - @ApiResponse({ status: 400, description: 'Invalid coupon code' }) async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) { return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto); } @@ -110,9 +95,6 @@ export class CartController { @Patch('address') @ApiOperation({ summary: 'Set address for cart' }) @ApiBody({ type: SetAddressDto }) - @ApiResponse({ status: 200, description: 'Address set successfully' }) - @ApiResponse({ status: 400, description: 'Bad request (e.g., address does not belong to user)' }) - @ApiResponse({ status: 404, description: 'Address not found' }) async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) { return this.cartService.setAddress(userId, restaurantId, setAddressDto); } @@ -120,9 +102,6 @@ export class CartController { @Patch('payment-method') @ApiOperation({ summary: 'Set payment method for cart' }) @ApiBody({ type: SetPaymentMethodDto }) - @ApiResponse({ status: 200, description: 'Payment method set successfully' }) - @ApiResponse({ status: 400, description: 'Bad request (e.g., payment method is not active)' }) - @ApiResponse({ status: 404, description: 'Payment method not found for restaurant' }) async setPaymentMethod( @UserId() userId: string, @RestId() restaurantId: string, diff --git a/src/modules/cart/interfaces/cart.interface.ts b/src/modules/cart/interfaces/cart.interface.ts index f8196e6..6b7e867 100644 --- a/src/modules/cart/interfaces/cart.interface.ts +++ b/src/modules/cart/interfaces/cart.interface.ts @@ -1,3 +1,5 @@ +import type { OrderCouponDetail } from 'src/modules/orders/interface/order-status'; + export interface CartItem { foodId: string; foodTitle?: string; @@ -7,28 +9,25 @@ export interface CartItem { totalPrice: number; } -export interface CartCoupon { - code: string; - discount: number; - discountType: 'amount' | 'percentage'; -} - export interface Cart { userId: string; restaurantId: string; restaurantName?: string; + items: CartItem[]; - coupon?: CartCoupon; + coupon?: OrderCouponDetail; addressId?: string; paymentMethodId?: string; deliveryMethodId?: string; + + deliveryFee: number; + subTotal: number; + tax: number; couponDiscount: number; itemsDiscount: number; totalDiscount: number; - subTotal: number; - tax: number; - shipmentFee: number; total: number; + totalItems: number; createdAt: string; updatedAt: string; diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 1df7d3e..192ee04 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -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 { + async findOneOrFail(userId: string, restaurantId: string): Promise { const cart = await this.getCartByRestaurant(userId, restaurantId); if (!cart) { throw new NotFoundException('Cart not found'); } return cart; } + async findOne2(userId: string, restaurantId: string): Promise { 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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' && diff --git a/src/modules/coupons/controllers/coupon.controller.ts b/src/modules/coupons/controllers/coupon.controller.ts index 247d06e..8c8d4f3 100644 --- a/src/modules/coupons/controllers/coupon.controller.ts +++ b/src/modules/coupons/controllers/coupon.controller.ts @@ -3,7 +3,6 @@ import { CouponService } from '../providers/coupon.service'; import { CreateCouponDto } from '../dto/create-coupon.dto'; import { UpdateCouponDto } from '../dto/update-coupon.dto'; import { FindCouponsDto } from '../dto/find-coupons.dto'; -import { ValidateCouponDto } from '../dto/validate-coupon.dto'; import { ApiTags, ApiOperation, @@ -24,20 +23,6 @@ import { RestId } from 'src/common/decorators'; export class CouponController { constructor(private readonly couponService: CouponService) {} - @Post('public/coupons/validate') - @ApiOperation({ summary: 'Validate a coupon code' }) - @ApiBody({ type: ValidateCouponDto }) - @ApiOkResponse({ description: 'Coupon validation result' }) - @ApiNotFoundResponse({ description: 'Coupon not found or invalid' }) - validateCoupon(@Body() validateCouponDto: ValidateCouponDto, @RestId() restId: string) { - return this.couponService.validateCoupon( - validateCouponDto.code, - restId, - validateCouponDto.orderAmount || 0, - validateCouponDto.userId, - ); - } - @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Post('admin/coupons') @@ -96,8 +81,6 @@ export class CouponController { return this.couponService.remove(id); } - - @Get('admin/coupons/generate-code') @ApiOperation({ summary: 'generate a coupon code' }) generateCouponCode(@RestId() restId: string) { diff --git a/src/modules/coupons/providers/coupon.service.ts b/src/modules/coupons/providers/coupon.service.ts index f1dacc8..e60c693 100644 --- a/src/modules/coupons/providers/coupon.service.ts +++ b/src/modules/coupons/providers/coupon.service.ts @@ -154,12 +154,10 @@ export class CouponService { async validateCoupon( code: string, restId: string, - orderAmount: number = 0, - userId?: string, + orderAmount: number, ): Promise<{ valid: boolean; coupon?: Coupon; - discount: number; message?: string; }> { const coupon = await this.couponRepository.findOne( @@ -170,7 +168,6 @@ export class CouponService { if (!coupon) { return { valid: false, - discount: 0, message: CouponMessage.NOT_FOUND, }; } @@ -179,7 +176,6 @@ export class CouponService { if (!coupon.isActive) { return { valid: false, - discount: 0, message: CouponMessage.COUPON_INACTIVE, }; } @@ -189,7 +185,6 @@ export class CouponService { if (coupon.startDate && now < coupon.startDate) { return { valid: false, - discount: 0, message: CouponMessage.COUPON_NOT_STARTED, }; } @@ -197,7 +192,6 @@ export class CouponService { if (coupon.endDate && now > coupon.endDate) { return { valid: false, - discount: 0, message: CouponMessage.COUPON_EXPIRED, }; } @@ -206,7 +200,6 @@ export class CouponService { if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { return { valid: false, - discount: 0, message: CouponMessage.COUPON_MAX_USES_REACHED, }; } @@ -215,29 +208,13 @@ export class CouponService { if (coupon.minOrderAmount && orderAmount < coupon.minOrderAmount) { return { valid: false, - discount: 0, message: CouponMessage.MIN_ORDER_AMOUNT_NOT_MET, }; } - // Calculate discount - let discount = 0; - if (coupon.type === CouponType.PERCENTAGE) { - discount = (orderAmount * coupon.value) / 100; - if (coupon.maxDiscount && discount > coupon.maxDiscount) { - discount = coupon.maxDiscount; - } - } else if (coupon.type === CouponType.FIXED) { - discount = coupon.value; - if (discount > orderAmount) { - discount = orderAmount; - } - } - return { valid: true, coupon, - discount: Math.round(discount), }; } @@ -253,7 +230,7 @@ export class CouponService { } async generateCouponCode(restId: string): Promise { - const code = randomBytes(8).toString('hex'); + const code = randomBytes(6).toString('hex'); const existingCoupon = await this.couponRepository.findOne({ code, restaurant: { id: restId } }); if (existingCoupon) { return this.generateCouponCode(restId); diff --git a/src/modules/orders/entities/order.entity.ts b/src/modules/orders/entities/order.entity.ts index 399ac0c..2cee2e1 100644 --- a/src/modules/orders/entities/order.entity.ts +++ b/src/modules/orders/entities/order.entity.ts @@ -12,7 +12,7 @@ import { } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { PaymentStatusEnum } from '../../payments/interface/payment'; -import { OrderStatus } from '../interface/order-status'; +import { OrderCouponDetail, OrderStatus } from '../interface/order-status'; import { User } from '../../users/entities/user.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { UserAddress } from '../../users/entities/user-address.entity'; @@ -51,17 +51,7 @@ export class Order extends BaseEntity { couponDiscount: number = 0; @Property({ type: 'jsonb', nullable: true }) - couponDetail: { - couponId: string; - couponName: string; - couponCode: string; - type: 'PERCENTAGE' | 'FIXED' | 'DELIVERY' | 'ITEM'; - value: number; - - maxDiscount?: number; - calculatedDiscount: number; - ruleVersion?: string; - } | null = null; + couponDetail?: OrderCouponDetail | null; @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) itemsDiscount: number = 0; @@ -76,7 +66,7 @@ export class Order extends BaseEntity { tax: number = 0; @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - shipmentFee: number = 0; + deliveryFee: number = 0; @Property({ type: 'decimal', precision: 10, scale: 0 }) total!: number; diff --git a/src/modules/orders/interface/order-status.ts b/src/modules/orders/interface/order-status.ts index 77caad1..d0e42dd 100644 --- a/src/modules/orders/interface/order-status.ts +++ b/src/modules/orders/interface/order-status.ts @@ -1,3 +1,5 @@ +import type { CouponType } from 'src/modules/coupons/entities/coupon.entity'; + export enum OrderStatus { Pending = 'pending', @@ -5,7 +7,7 @@ export enum OrderStatus { Confirmed = 'confirmed', Preparing = 'preparing', - + RejectedByRestaurant = 'rejectedByRestaurant', CancelledByUser = 'cancelledByUser', CancelledBySystem = 'cancelledBySystem', @@ -17,3 +19,13 @@ export enum OrderStatus { Delivered = 'delivered', } + +export interface OrderCouponDetail { + couponId: string; + couponName: string; + couponCode: string; + type: CouponType; + value: number; + + maxDiscount?: number; + } diff --git a/src/modules/orders/orders.service.ts b/src/modules/orders/orders.service.ts index 41086a0..0698839 100644 --- a/src/modules/orders/orders.service.ts +++ b/src/modules/orders/orders.service.ts @@ -33,7 +33,7 @@ export class OrdersService { ) {} async checkout(userId: string, restaurantId: string) { - const cart = await this.cartService.findOne(userId, restaurantId); + const cart = await this.cartService.findOneOrFail(userId, restaurantId); const { order } = await this.createOrder(userId, restaurantId, cart); @@ -64,7 +64,7 @@ export class OrdersService { totalDiscount: cart.totalDiscount || 0, subTotal: cart.subTotal || 0, tax: cart.tax || 0, - shipmentFee: cart.shipmentFee || 0, + deliveryFee: cart.deliveryFee || 0, total: cart.total || 0, totalItems: cart.totalItems || 0, status: OrderStatus.Pending,