cart and coupon
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CouponType } from 'src/modules/coupons/entities/coupon.entity';
|
||||||
|
|
||||||
export enum OrderStatus {
|
export enum OrderStatus {
|
||||||
Pending = 'pending',
|
Pending = 'pending',
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user