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
+2
View File
@@ -9,6 +9,7 @@ import { UserAddress } from '../users/entities/user-address.entity';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { UtilsModule } from '../utils/utils.module'; import { UtilsModule } from '../utils/utils.module';
import { CouponModule } from '../coupons/coupon.module';
@Module({ @Module({
imports: [ imports: [
@@ -16,6 +17,7 @@ import { UtilsModule } from '../utils/utils.module';
AuthModule, AuthModule,
JwtModule, JwtModule,
UtilsModule, UtilsModule,
CouponModule,
], ],
controllers: [CartController], controllers: [CartController],
providers: [CartService], providers: [CartService],
@@ -20,8 +20,6 @@ export class CartController {
@Get() @Get()
@ApiOperation({ summary: 'Get cart for current user and restaurant' }) @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) { async findOne(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.getOrCreateCart(userId, restaurantId); return this.cartService.getOrCreateCart(userId, restaurantId);
} }
@@ -29,9 +27,6 @@ export class CartController {
@Post('items/:foodId/increment') @Post('items/:foodId/increment')
@ApiOperation({ summary: 'Increment item quantity in cart by 1' }) @ApiOperation({ summary: 'Increment item quantity in cart by 1' })
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' }) @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) { 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);
} }
@@ -39,8 +34,6 @@ export class CartController {
@Post('items/:foodId/decrement') @Post('items/:foodId/decrement')
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' }) @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' }) @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) { async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.decrementItem(userId, restaurantId, foodId); return this.cartService.decrementItem(userId, restaurantId, foodId);
} }
@@ -48,9 +41,6 @@ export class CartController {
@Post('items/bulk') @Post('items/bulk')
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' }) @ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
@ApiBody({ type: BulkAddItemsToCartDto }) @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( bulkAddItems(
@UserId() userId: string, @UserId() userId: string,
@RestId() restaurantId: string, @RestId() restaurantId: string,
@@ -63,9 +53,6 @@ export class CartController {
@ApiOperation({ summary: 'Update item quantity in cart' }) @ApiOperation({ summary: 'Update item quantity in cart' })
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' }) @ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
@ApiBody({ type: UpdateItemQuantityDto }) @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( async updateItemQuantity(
@UserId() userId: string, @UserId() userId: string,
@RestId() restaurantId: string, @RestId() restaurantId: string,
@@ -94,8 +81,6 @@ export class CartController {
@Post('apply-coupon') @Post('apply-coupon')
@ApiOperation({ summary: 'Apply coupon to cart' }) @ApiOperation({ summary: 'Apply coupon to cart' })
@ApiBody({ type: ApplyCouponDto }) @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) { async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto); return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
} }
@@ -110,9 +95,6 @@ export class CartController {
@Patch('address') @Patch('address')
@ApiOperation({ summary: 'Set address for cart' }) @ApiOperation({ summary: 'Set address for cart' })
@ApiBody({ type: SetAddressDto }) @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) { async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) {
return this.cartService.setAddress(userId, restaurantId, setAddressDto); return this.cartService.setAddress(userId, restaurantId, setAddressDto);
} }
@@ -120,9 +102,6 @@ export class CartController {
@Patch('payment-method') @Patch('payment-method')
@ApiOperation({ summary: 'Set payment method for cart' }) @ApiOperation({ summary: 'Set payment method for cart' })
@ApiBody({ type: SetPaymentMethodDto }) @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( async setPaymentMethod(
@UserId() userId: string, @UserId() userId: string,
@RestId() restaurantId: string, @RestId() restaurantId: string,
+9 -10
View File
@@ -1,3 +1,5 @@
import type { OrderCouponDetail } from 'src/modules/orders/interface/order-status';
export interface CartItem { export interface CartItem {
foodId: string; foodId: string;
foodTitle?: string; foodTitle?: string;
@@ -7,28 +9,25 @@ export interface CartItem {
totalPrice: number; totalPrice: number;
} }
export interface CartCoupon {
code: string;
discount: number;
discountType: 'amount' | 'percentage';
}
export interface Cart { export interface Cart {
userId: string; userId: string;
restaurantId: string; restaurantId: string;
restaurantName?: string; restaurantName?: string;
items: CartItem[]; items: CartItem[];
coupon?: CartCoupon; coupon?: OrderCouponDetail;
addressId?: string; addressId?: string;
paymentMethodId?: string; paymentMethodId?: string;
deliveryMethodId?: string; deliveryMethodId?: string;
deliveryFee: number;
subTotal: number;
tax: number;
couponDiscount: number; couponDiscount: number;
itemsDiscount: number; itemsDiscount: number;
totalDiscount: number; totalDiscount: number;
subTotal: number;
tax: number;
shipmentFee: number;
total: number; total: number;
totalItems: number; totalItems: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
+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 { UserAddress } from '../../users/entities/user-address.entity';
import { PaymentMethod } from '../../payments/entities/payment-method.entity'; import { PaymentMethod } from '../../payments/entities/payment-method.entity';
import { Delivery } from '../../delivery/entities/delivery.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() @Injectable()
export class CartService { export class CartService {
@@ -23,6 +26,7 @@ export class CartService {
constructor( constructor(
private readonly em: EntityManager, private readonly em: EntityManager,
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly couponService: CouponService,
) {} ) {}
/** /**
@@ -47,7 +51,7 @@ export class CartService {
restaurantId, restaurantId,
restaurantName: restaurant.name, restaurantName: restaurant.name,
items: [], items: [],
shipmentFee: 0, deliveryFee: 0,
subTotal: 0, subTotal: 0,
itemsDiscount: 0, itemsDiscount: 0,
couponDiscount: 0, couponDiscount: 0,
@@ -66,13 +70,14 @@ export class CartService {
/** /**
* Find cart by user and restaurant * 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); const cart = await this.getCartByRestaurant(userId, restaurantId);
if (!cart) { if (!cart) {
throw new NotFoundException('Cart not found'); throw new NotFoundException('Cart not found');
} }
return cart; return cart;
} }
async findOne2(userId: string, restaurantId: string): Promise<Cart | null> { async findOne2(userId: string, restaurantId: string): Promise<Cart | null> {
const cart = await this.getCartByRestaurant(userId, restaurantId); const cart = await this.getCartByRestaurant(userId, restaurantId);
return cart; return cart;
@@ -162,7 +167,7 @@ export class CartService {
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0) * Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
*/ */
async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> { 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); const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) { if (itemIndex < 0) {
@@ -287,7 +292,7 @@ export class CartService {
foodId: string, foodId: string,
updateItemDto: UpdateItemQuantityDto, updateItemDto: UpdateItemQuantityDto,
): Promise<Cart> { ): 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); const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) { if (itemIndex < 0) {
@@ -331,7 +336,7 @@ export class CartService {
* Remove item from cart * Remove item from cart
*/ */
async removeItem(userId: string, restaurantId: string, foodId: string): Promise<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); const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
if (itemIndex < 0) { if (itemIndex < 0) {
@@ -352,20 +357,60 @@ export class CartService {
* Apply coupon to cart * Apply coupon to cart
*/ */
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<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 // Calculate current order amount (subtotal after item discounts) for coupon validation
// For now, this is a placeholder implementation // const currentSubTotal = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// You should validate the coupon code against your coupon/discount system // 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 orderAmount = cart.subTotal - cart.itemsDiscount;
const coupon: CartCoupon = {
code: applyCouponDto.code, const { valid, coupon } = await this.couponService.validateCoupon(
discount: 10, // Example: 10% discount applyCouponDto.code,
discountType: 'percentage', // or 'amount' 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 // Recalculate cart totals
await this.recalculateCartTotals(cart); await this.recalculateCartTotals(cart);
@@ -378,7 +423,7 @@ export class CartService {
* Remove coupon from cart * Remove coupon from cart
*/ */
async removeCoupon(userId: string, restaurantId: string): Promise<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; cart.coupon = undefined;
@@ -393,7 +438,7 @@ export class CartService {
* Set address for cart * Set address for cart
*/ */
async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise<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 // Find and validate address belongs to user
const address = await this.em.findOne(UserAddress, { id: setAddressDto.addressId }, { populate: ['user'] }); const address = await this.em.findOne(UserAddress, { id: setAddressDto.addressId }, { populate: ['user'] });
@@ -422,7 +467,7 @@ export class CartService {
restaurantId: string, restaurantId: string,
setPaymentMethodDto: SetPaymentMethodDto, setPaymentMethodDto: SetPaymentMethodDto,
): Promise<Cart> { ): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
// Find and validate payment method belongs to restaurant // Find and validate payment method belongs to restaurant
const paymentMethod = await this.em.findOne( const paymentMethod = await this.em.findOne(
@@ -462,7 +507,7 @@ export class CartService {
restaurantId: string, restaurantId: string,
setDeliveryMethodDto: SetDeliveryMethodDto, setDeliveryMethodDto: SetDeliveryMethodDto,
): Promise<Cart> { ): Promise<Cart> {
const cart = await this.findOne(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
// Find and validate delivery method belongs to restaurant // Find and validate delivery method belongs to restaurant
const deliveryMethod = await this.em.findOne( const deliveryMethod = await this.em.findOne(
@@ -487,7 +532,7 @@ export class CartService {
cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId; 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.recalculateCartTotals(cart);
await this.saveCart(cart); await this.saveCart(cart);
@@ -515,14 +560,19 @@ export class CartService {
// Calculate coupon discount // Calculate coupon discount
let couponDiscount = 0; let couponDiscount = 0;
if (cart.coupon) { if (cart.coupon) {
if (cart.coupon.discountType === 'percentage') { const priceAfterItemDiscount = subTotal - itemsDiscount;
const priceAfterItemDiscount = subTotal - itemsDiscount; if (cart.coupon.type === CouponType.PERCENTAGE) {
couponDiscount = (priceAfterItemDiscount * cart.coupon.discount) / 100; 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 { } else {
// amount discount // FIXED amount discount
const priceAfterItemDiscount = subTotal - itemsDiscount; couponDiscount = Math.min(cart.coupon.value, priceAfterItemDiscount);
couponDiscount = Math.min(cart.coupon.discount, priceAfterItemDiscount);
} }
} }
@@ -542,17 +592,17 @@ export class CartService {
cart.tax = tax; cart.tax = tax;
// Shipment fee is calculated separately via delivery method // Shipment fee is calculated separately via delivery method
let shipmentFee = 0; let deliveryFee = 0;
if (cart.deliveryMethodId) { if (cart.deliveryMethodId) {
const deliveryMethod = await this.em.findOne(Delivery, { id: cart.deliveryMethodId }); const deliveryMethod = await this.em.findOne(Delivery, { id: cart.deliveryMethodId });
if (deliveryMethod && deliveryMethod.enabled) { 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 // Calculate total: total = subtotal totalDiscount + tax + deliveryFee
cart.total = subTotal - cart.totalDiscount + tax + cart.shipmentFee; cart.total = subTotal - cart.totalDiscount + tax + cart.deliveryFee;
cart.totalItems = totalItems; cart.totalItems = totalItems;
cart.updatedAt = new Date().toISOString(); cart.updatedAt = new Date().toISOString();
} }
@@ -574,7 +624,7 @@ export class CartService {
typeof cart.couponDiscount === 'number' && typeof cart.couponDiscount === 'number' &&
typeof cart.totalDiscount === 'number' && typeof cart.totalDiscount === 'number' &&
typeof cart.tax === 'number' && typeof cart.tax === 'number' &&
typeof cart.shipmentFee === 'number' && typeof cart.deliveryFee === 'number' &&
typeof cart.total === 'number' && typeof cart.total === 'number' &&
typeof cart.totalItems === 'number' && typeof cart.totalItems === 'number' &&
typeof cart.createdAt === 'string' && typeof cart.createdAt === 'string' &&
@@ -3,7 +3,6 @@ import { CouponService } from '../providers/coupon.service';
import { CreateCouponDto } from '../dto/create-coupon.dto'; import { CreateCouponDto } from '../dto/create-coupon.dto';
import { UpdateCouponDto } from '../dto/update-coupon.dto'; import { UpdateCouponDto } from '../dto/update-coupon.dto';
import { FindCouponsDto } from '../dto/find-coupons.dto'; import { FindCouponsDto } from '../dto/find-coupons.dto';
import { ValidateCouponDto } from '../dto/validate-coupon.dto';
import { import {
ApiTags, ApiTags,
ApiOperation, ApiOperation,
@@ -24,20 +23,6 @@ import { RestId } from 'src/common/decorators';
export class CouponController { export class CouponController {
constructor(private readonly couponService: CouponService) {} 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) @UseGuards(AdminAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@Post('admin/coupons') @Post('admin/coupons')
@@ -96,8 +81,6 @@ export class CouponController {
return this.couponService.remove(id); return this.couponService.remove(id);
} }
@Get('admin/coupons/generate-code') @Get('admin/coupons/generate-code')
@ApiOperation({ summary: 'generate a coupon code' }) @ApiOperation({ summary: 'generate a coupon code' })
generateCouponCode(@RestId() restId: string) { generateCouponCode(@RestId() restId: string) {
@@ -154,12 +154,10 @@ export class CouponService {
async validateCoupon( async validateCoupon(
code: string, code: string,
restId: string, restId: string,
orderAmount: number = 0, orderAmount: number,
userId?: string,
): Promise<{ ): Promise<{
valid: boolean; valid: boolean;
coupon?: Coupon; coupon?: Coupon;
discount: number;
message?: string; message?: string;
}> { }> {
const coupon = await this.couponRepository.findOne( const coupon = await this.couponRepository.findOne(
@@ -170,7 +168,6 @@ export class CouponService {
if (!coupon) { if (!coupon) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.NOT_FOUND, message: CouponMessage.NOT_FOUND,
}; };
} }
@@ -179,7 +176,6 @@ export class CouponService {
if (!coupon.isActive) { if (!coupon.isActive) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.COUPON_INACTIVE, message: CouponMessage.COUPON_INACTIVE,
}; };
} }
@@ -189,7 +185,6 @@ export class CouponService {
if (coupon.startDate && now < coupon.startDate) { if (coupon.startDate && now < coupon.startDate) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.COUPON_NOT_STARTED, message: CouponMessage.COUPON_NOT_STARTED,
}; };
} }
@@ -197,7 +192,6 @@ export class CouponService {
if (coupon.endDate && now > coupon.endDate) { if (coupon.endDate && now > coupon.endDate) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.COUPON_EXPIRED, message: CouponMessage.COUPON_EXPIRED,
}; };
} }
@@ -206,7 +200,6 @@ export class CouponService {
if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.COUPON_MAX_USES_REACHED, message: CouponMessage.COUPON_MAX_USES_REACHED,
}; };
} }
@@ -215,29 +208,13 @@ export class CouponService {
if (coupon.minOrderAmount && orderAmount < coupon.minOrderAmount) { if (coupon.minOrderAmount && orderAmount < coupon.minOrderAmount) {
return { return {
valid: false, valid: false,
discount: 0,
message: CouponMessage.MIN_ORDER_AMOUNT_NOT_MET, 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 { return {
valid: true, valid: true,
coupon, coupon,
discount: Math.round(discount),
}; };
} }
@@ -253,7 +230,7 @@ export class CouponService {
} }
async generateCouponCode(restId: string): Promise<string> { async generateCouponCode(restId: string): Promise<string> {
const code = randomBytes(8).toString('hex'); const code = randomBytes(6).toString('hex');
const existingCoupon = await this.couponRepository.findOne({ code, restaurant: { id: restId } }); const existingCoupon = await this.couponRepository.findOne({ code, restaurant: { id: restId } });
if (existingCoupon) { if (existingCoupon) {
return this.generateCouponCode(restId); return this.generateCouponCode(restId);
+3 -13
View File
@@ -12,7 +12,7 @@ import {
} from '@mikro-orm/core'; } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { PaymentStatusEnum } from '../../payments/interface/payment'; 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 { User } from '../../users/entities/user.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { UserAddress } from '../../users/entities/user-address.entity'; import { UserAddress } from '../../users/entities/user-address.entity';
@@ -51,17 +51,7 @@ export class Order extends BaseEntity {
couponDiscount: number = 0; couponDiscount: number = 0;
@Property({ type: 'jsonb', nullable: true }) @Property({ type: 'jsonb', nullable: true })
couponDetail: { couponDetail?: OrderCouponDetail | null;
couponId: string;
couponName: string;
couponCode: string;
type: 'PERCENTAGE' | 'FIXED' | 'DELIVERY' | 'ITEM';
value: number;
maxDiscount?: number;
calculatedDiscount: number;
ruleVersion?: string;
} | null = null;
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
itemsDiscount: number = 0; itemsDiscount: number = 0;
@@ -76,7 +66,7 @@ export class Order extends BaseEntity {
tax: number = 0; tax: number = 0;
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
shipmentFee: number = 0; deliveryFee: number = 0;
@Property({ type: 'decimal', precision: 10, scale: 0 }) @Property({ type: 'decimal', precision: 10, scale: 0 })
total!: number; total!: number;
+13 -1
View File
@@ -1,3 +1,5 @@
import type { CouponType } from 'src/modules/coupons/entities/coupon.entity';
export enum OrderStatus { export enum OrderStatus {
Pending = 'pending', Pending = 'pending',
@@ -5,7 +7,7 @@ export enum OrderStatus {
Confirmed = 'confirmed', Confirmed = 'confirmed',
Preparing = 'preparing', Preparing = 'preparing',
RejectedByRestaurant = 'rejectedByRestaurant', RejectedByRestaurant = 'rejectedByRestaurant',
CancelledByUser = 'cancelledByUser', CancelledByUser = 'cancelledByUser',
CancelledBySystem = 'cancelledBySystem', CancelledBySystem = 'cancelledBySystem',
@@ -17,3 +19,13 @@ export enum OrderStatus {
Delivered = 'delivered', Delivered = 'delivered',
} }
export interface OrderCouponDetail {
couponId: string;
couponName: string;
couponCode: string;
type: CouponType;
value: number;
maxDiscount?: number;
}
+2 -2
View File
@@ -33,7 +33,7 @@ export class OrdersService {
) {} ) {}
async checkout(userId: string, restaurantId: string) { 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); const { order } = await this.createOrder(userId, restaurantId, cart);
@@ -64,7 +64,7 @@ export class OrdersService {
totalDiscount: cart.totalDiscount || 0, totalDiscount: cart.totalDiscount || 0,
subTotal: cart.subTotal || 0, subTotal: cart.subTotal || 0,
tax: cart.tax || 0, tax: cart.tax || 0,
shipmentFee: cart.shipmentFee || 0, deliveryFee: cart.deliveryFee || 0,
total: cart.total || 0, total: cart.total || 0,
totalItems: cart.totalItems || 0, totalItems: cart.totalItems || 0,
status: OrderStatus.Pending, status: OrderStatus.Pending,