refactor cart
This commit is contained in:
@@ -636,6 +636,7 @@ export const enum CouponMessage {
|
|||||||
COUPON_DELETED = 'کوپن با موفقیت حذف شد',
|
COUPON_DELETED = 'کوپن با موفقیت حذف شد',
|
||||||
COUPON_VALID = 'کوپن معتبر است',
|
COUPON_VALID = 'کوپن معتبر است',
|
||||||
COUPON_INVALID = 'کوپن نامعتبر است',
|
COUPON_INVALID = 'کوپن نامعتبر است',
|
||||||
|
COUPON_RESTRICTED_TO_USER = 'کوپن فقط برای کاربر مشخصی معتبر است',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ReviewMessage {
|
export const enum ReviewMessage {
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
|
||||||
import { CartService } from '../providers/cart.service';
|
import { CartService } from '../providers/cart.service';
|
||||||
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
||||||
import { UpdateItemQuantityDto } from '../dto/update-item.dto';
|
|
||||||
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
||||||
import { SetAddressDto } from '../dto/set-address.dto';
|
import { SetAddressDto } from '../dto/set-address.dto';
|
||||||
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
|
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
|
||||||
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
|
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
|
||||||
import { SetDescriptionDto } from '../dto/set-description.dto';
|
import { SetDescriptionDto } from '../dto/set-description.dto';
|
||||||
import { SetTableNumberDto } from '../dto/set-table-number.dto';
|
import { SetTableNumberDto } from '../dto/set-table-number.dto';
|
||||||
|
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
|
||||||
import { AuthGuard } from '../../auth/guards/auth.guard';
|
import { AuthGuard } from '../../auth/guards/auth.guard';
|
||||||
import { UserId } from 'src/common/decorators/user-id.decorator';
|
import { UserId } from 'src/common/decorators/user-id.decorator';
|
||||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||||
|
|
||||||
|
const API_HEADER_SLUG = {
|
||||||
|
name: 'X-Slug',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'zhivan',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiTags('cart')
|
@ApiTags('cart')
|
||||||
@@ -22,43 +31,22 @@ export class CartController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Get cart for current user and restaurant' })
|
@ApiOperation({ summary: 'Get cart for current user and restaurant' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
|
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
|
||||||
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, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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)' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' })
|
@ApiParam({ name: 'foodId', description: 'Food ID to decrement 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);
|
||||||
@@ -66,14 +54,7 @@ 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)' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: BulkAddItemsToCartDto })
|
@ApiBody({ type: BulkAddItemsToCartDto })
|
||||||
bulkAddItems(
|
bulkAddItems(
|
||||||
@UserId() userId: string,
|
@UserId() userId: string,
|
||||||
@@ -83,37 +64,9 @@ export class CartController {
|
|||||||
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
|
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('items/:foodId')
|
|
||||||
@ApiOperation({ summary: 'Update item quantity in cart' })
|
|
||||||
@ApiHeader({
|
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
|
|
||||||
@ApiBody({ type: UpdateItemQuantityDto })
|
|
||||||
async updateItemQuantity(
|
|
||||||
@UserId() userId: string,
|
|
||||||
@RestId() restaurantId: string,
|
|
||||||
@Param('foodId') foodId: string,
|
|
||||||
@Body() updateItemDto: UpdateItemQuantityDto,
|
|
||||||
) {
|
|
||||||
return this.cartService.updateItemQuantity(userId, restaurantId, foodId, updateItemDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('items/:foodId')
|
@Delete('items/:foodId')
|
||||||
@ApiOperation({ summary: 'Remove item from cart' })
|
@ApiOperation({ summary: 'Remove item from cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
|
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
|
||||||
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
|
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
|
||||||
return this.cartService.removeItem(userId, restaurantId, foodId);
|
return this.cartService.removeItem(userId, restaurantId, foodId);
|
||||||
@@ -121,28 +74,14 @@ export class CartController {
|
|||||||
|
|
||||||
@Delete()
|
@Delete()
|
||||||
@ApiOperation({ summary: 'Clear entire cart' })
|
@ApiOperation({ summary: 'Clear entire cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
|
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||||
return this.cartService.clearCart(userId, restaurantId);
|
return this.cartService.clearCart(userId, restaurantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('apply-coupon')
|
@Post('apply-coupon')
|
||||||
@ApiOperation({ summary: 'Apply coupon to cart' })
|
@ApiOperation({ summary: 'Apply coupon to cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: ApplyCouponDto })
|
@ApiBody({ type: ApplyCouponDto })
|
||||||
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);
|
||||||
@@ -150,28 +89,14 @@ export class CartController {
|
|||||||
|
|
||||||
@Delete('coupon')
|
@Delete('coupon')
|
||||||
@ApiOperation({ summary: 'Remove coupon from cart' })
|
@ApiOperation({ summary: 'Remove coupon from cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
|
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||||
return this.cartService.removeCoupon(userId, restaurantId);
|
return this.cartService.removeCoupon(userId, restaurantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('address')
|
@Patch('address')
|
||||||
@ApiOperation({ summary: 'Set address for cart' })
|
@ApiOperation({ summary: 'Set address for cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: SetAddressDto })
|
@ApiBody({ type: SetAddressDto })
|
||||||
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);
|
||||||
@@ -179,14 +104,7 @@ export class CartController {
|
|||||||
|
|
||||||
@Patch('payment-method')
|
@Patch('payment-method')
|
||||||
@ApiOperation({ summary: 'Set payment method for cart' })
|
@ApiOperation({ summary: 'Set payment method for cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: SetPaymentMethodDto })
|
@ApiBody({ type: SetPaymentMethodDto })
|
||||||
async setPaymentMethod(
|
async setPaymentMethod(
|
||||||
@UserId() userId: string,
|
@UserId() userId: string,
|
||||||
@@ -198,14 +116,7 @@ export class CartController {
|
|||||||
|
|
||||||
@Patch('delivery-method')
|
@Patch('delivery-method')
|
||||||
@ApiOperation({ summary: 'Set delivery method for cart' })
|
@ApiOperation({ summary: 'Set delivery method for cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: SetDeliveryMethodDto })
|
@ApiBody({ type: SetDeliveryMethodDto })
|
||||||
setDeliveryMethod(
|
setDeliveryMethod(
|
||||||
@UserId() userId: string,
|
@UserId() userId: string,
|
||||||
@@ -217,14 +128,7 @@ export class CartController {
|
|||||||
|
|
||||||
@Patch('description')
|
@Patch('description')
|
||||||
@ApiOperation({ summary: 'Set description for cart' })
|
@ApiOperation({ summary: 'Set description for cart' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: SetDescriptionDto })
|
@ApiBody({ type: SetDescriptionDto })
|
||||||
setDescription(
|
setDescription(
|
||||||
@UserId() userId: string,
|
@UserId() userId: string,
|
||||||
@@ -236,14 +140,7 @@ export class CartController {
|
|||||||
|
|
||||||
@Patch('table-number')
|
@Patch('table-number')
|
||||||
@ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' })
|
@ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' })
|
||||||
@ApiHeader({
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
name: 'X-Slug',
|
|
||||||
required: true,
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'zhivan',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiBody({ type: SetTableNumberDto })
|
@ApiBody({ type: SetTableNumberDto })
|
||||||
setTableNumber(
|
setTableNumber(
|
||||||
@UserId() userId: string,
|
@UserId() userId: string,
|
||||||
@@ -252,4 +149,16 @@ export class CartController {
|
|||||||
) {
|
) {
|
||||||
return this.cartService.setTableNumber(userId, restaurantId, setTableNumberDto);
|
return this.cartService.setTableNumber(userId, restaurantId, setTableNumberDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('car-delivery')
|
||||||
|
@ApiOperation({ summary: 'Set car delivery for cart' })
|
||||||
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
|
@ApiBody({ type: SetCarDeliveryDto })
|
||||||
|
setCarDelivery(
|
||||||
|
@UserId() userId: string,
|
||||||
|
@RestId() restaurantId: string,
|
||||||
|
@Body() setCarDeliveryDto: SetCarDeliveryDto,
|
||||||
|
) {
|
||||||
|
return this.cartService.setCarDelivery(userId, restaurantId, setCarDeliveryDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
|
||||||
|
|
||||||
export class AddItemToCartDto {
|
|
||||||
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
quantity!: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Food ID' })
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
foodId!: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
|
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsNumber, Min, IsString } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { AddItemToCartDto } from './add-item.dto';
|
|
||||||
|
class AddItemToCartDto {
|
||||||
|
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
quantity!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Food ID' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
foodId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class BulkAddItemsToCartDto {
|
export class BulkAddItemsToCartDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString, IsNumber, IsLatitude, IsLongitude } from 'class-validator';
|
||||||
|
|
||||||
|
export class SetCarDeliveryDto {
|
||||||
|
@ApiProperty({ description: 'Car model', example: 'Toyota Camry' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
carModel!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Car color', example: 'White' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
carColor!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'License plate number', example: '12ABC345' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
plateNumber!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Delivery address', example: '123 Main Street, City' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
address!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Latitude coordinate', example: 35.6892 })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@IsLatitude()
|
||||||
|
latitude!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Longitude coordinate', example: 51.389 })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@IsLongitude()
|
||||||
|
longitude!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Full name of the recipient', example: 'John Doe' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
fullName!: string;
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsNotEmpty, IsNumber, Min } from 'class-validator';
|
|
||||||
|
|
||||||
export class UpdateItemQuantityDto {
|
|
||||||
@ApiProperty({ description: 'Quantity of the food item', example: 2, minimum: 1 })
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
quantity!: number;
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ export interface Cart {
|
|||||||
|
|
||||||
items: CartItem[];
|
items: CartItem[];
|
||||||
coupon?: OrderCouponDetail;
|
coupon?: OrderCouponDetail;
|
||||||
addressId?: string;
|
|
||||||
paymentMethodId?: string;
|
paymentMethodId?: string;
|
||||||
deliveryMethodId?: string;
|
deliveryMethodId?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -30,6 +29,28 @@ export interface Cart {
|
|||||||
totalDiscount: number;
|
totalDiscount: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
|
carAddress?: {
|
||||||
|
carModel: string;
|
||||||
|
carColor: string;
|
||||||
|
plateNumber: string;
|
||||||
|
address?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
fullName: string;
|
||||||
|
phone: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
userAddress?: {
|
||||||
|
address?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
postalCode: string;
|
||||||
|
fullName: string;
|
||||||
|
phone: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import { Food } from '../../foods/entities/food.entity';
|
|||||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||||
import { EntityManager } from '@mikro-orm/postgresql';
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
import { CacheService } from '../../utils/cache.service';
|
import { CacheService } from '../../utils/cache.service';
|
||||||
import { AddItemToCartDto } from '../dto/add-item.dto';
|
|
||||||
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
||||||
import { UpdateItemQuantityDto } from '../dto/update-item.dto';
|
|
||||||
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
||||||
import { SetAddressDto } from '../dto/set-address.dto';
|
import { SetAddressDto } from '../dto/set-address.dto';
|
||||||
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
|
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
|
||||||
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
|
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
|
||||||
import { SetDescriptionDto } from '../dto/set-description.dto';
|
import { SetDescriptionDto } from '../dto/set-description.dto';
|
||||||
import { SetTableNumberDto } from '../dto/set-table-number.dto';
|
import { SetTableNumberDto } from '../dto/set-table-number.dto';
|
||||||
|
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
|
||||||
import { UserAddress } from '../../users/entities/user-address.entity';
|
import { UserAddress } from '../../users/entities/user-address.entity';
|
||||||
import { User } from '../../users/entities/user.entity';
|
import { User } from '../../users/entities/user.entity';
|
||||||
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
||||||
@@ -82,92 +81,35 @@ export class CartService {
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne2(userId: string, restaurantId: string): Promise<Cart | null> {
|
|
||||||
const cart = await this.getCartByRestaurant(userId, restaurantId);
|
|
||||||
return cart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add item to cart (increment quantity if item exists, otherwise create new)
|
* Increment item quantity in cart by 1
|
||||||
*/
|
*/
|
||||||
async addItem(userId: string, restaurantId: string, foodId: string, addItemDto: AddItemToCartDto): Promise<Cart> {
|
async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
|
||||||
const cart = await this.getOrCreateCart(userId, restaurantId);
|
const cart = await this.getOrCreateCart(userId, restaurantId);
|
||||||
|
const food = await this.validateAndGetFood(foodId, restaurantId, quantity);
|
||||||
|
|
||||||
// Find food
|
|
||||||
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
|
|
||||||
if (!food) {
|
|
||||||
throw new NotFoundException(`Food with ID ${foodId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if food belongs to the restaurant
|
|
||||||
if (food.restaurant.id !== restaurantId) {
|
|
||||||
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check stock availability
|
|
||||||
const availableStock = food.inventory?.availableStock ?? 0;
|
|
||||||
if (availableStock < addItemDto.quantity) {
|
|
||||||
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if item already exists in cart
|
|
||||||
const existingItemIndex = cart.items.findIndex(item => item.foodId === foodId);
|
const existingItemIndex = cart.items.findIndex(item => item.foodId === foodId);
|
||||||
|
|
||||||
if (existingItemIndex >= 0) {
|
if (existingItemIndex >= 0) {
|
||||||
// Update existing item quantity
|
|
||||||
const existingItem = cart.items[existingItemIndex];
|
const existingItem = cart.items[existingItemIndex];
|
||||||
const newQuantity = existingItem.quantity + addItemDto.quantity;
|
const newQuantity = existingItem.quantity + quantity;
|
||||||
|
|
||||||
// Check stock for new total quantity
|
|
||||||
if (availableStock < newQuantity) {
|
|
||||||
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * newQuantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * newQuantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
|
this.validateStock(food, newQuantity);
|
||||||
cart.items[existingItemIndex] = {
|
cart.items[existingItemIndex] = {
|
||||||
...existingItem,
|
...existingItem,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
totalPrice: itemFinalPrice,
|
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Create new item
|
cart.items.push(this.createCartItem(food, quantity));
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * addItemDto.quantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * addItemDto.quantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
const cartItem: CartItem = {
|
|
||||||
foodId: food.id,
|
|
||||||
foodTitle: food.title,
|
|
||||||
quantity: addItemDto.quantity,
|
|
||||||
price: itemPrice,
|
|
||||||
discount: itemDiscount,
|
|
||||||
totalPrice: itemFinalPrice,
|
|
||||||
};
|
|
||||||
|
|
||||||
cart.items.push(cartItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate cart totals
|
|
||||||
await this.recalculateCartTotals(cart);
|
await this.recalculateCartTotals(cart);
|
||||||
await this.saveCart(cart);
|
await this.saveCart(cart);
|
||||||
|
|
||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment item quantity in cart by 1
|
|
||||||
*/
|
|
||||||
async incrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
|
|
||||||
return this.addItem(userId, restaurantId, foodId, { foodId, quantity: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
|
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
|
||||||
*/
|
*/
|
||||||
@@ -186,22 +128,15 @@ export class CartService {
|
|||||||
// Remove item if quantity becomes 0 or less
|
// Remove item if quantity becomes 0 or less
|
||||||
cart.items.splice(itemIndex, 1);
|
cart.items.splice(itemIndex, 1);
|
||||||
} else {
|
} else {
|
||||||
// Update item quantity and recalculate price
|
|
||||||
const food = await this.em.findOne(Food, { id: foodId });
|
const food = await this.em.findOne(Food, { id: foodId });
|
||||||
if (!food) {
|
if (!food) {
|
||||||
throw new NotFoundException(`Food with ID ${foodId} not found`);
|
throw new NotFoundException(`Food with ID ${foodId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * newQuantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * newQuantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
cart.items[itemIndex] = {
|
cart.items[itemIndex] = {
|
||||||
...existingItem,
|
...existingItem,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
totalPrice: itemFinalPrice,
|
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,121 +153,25 @@ export class CartService {
|
|||||||
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
|
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
|
||||||
const cart = await this.getOrCreateCart(userId, restaurantId);
|
const cart = await this.getOrCreateCart(userId, restaurantId);
|
||||||
|
|
||||||
// Process each item
|
|
||||||
for (const addItemDto of bulkAddItemsDto.items) {
|
for (const addItemDto of bulkAddItemsDto.items) {
|
||||||
// Find food
|
const food = await this.validateAndGetFood(addItemDto.foodId, restaurantId, addItemDto.quantity);
|
||||||
const food = await this.em.findOne(Food, { id: addItemDto.foodId }, { populate: ['restaurant', 'inventory'] });
|
|
||||||
if (!food) {
|
|
||||||
throw new NotFoundException(`Food with ID ${addItemDto.foodId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if food belongs to the restaurant
|
|
||||||
if (food.restaurant.id !== restaurantId) {
|
|
||||||
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check stock availability
|
|
||||||
const availableStock = food.inventory?.availableStock ?? 0;
|
|
||||||
if (availableStock < addItemDto.quantity) {
|
|
||||||
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if item already exists in cart
|
|
||||||
const existingItemIndex = cart.items.findIndex(item => item.foodId === addItemDto.foodId);
|
const existingItemIndex = cart.items.findIndex(item => item.foodId === addItemDto.foodId);
|
||||||
|
|
||||||
if (existingItemIndex >= 0) {
|
if (existingItemIndex >= 0) {
|
||||||
// Update existing item quantity
|
|
||||||
const existingItem = cart.items[existingItemIndex];
|
const existingItem = cart.items[existingItemIndex];
|
||||||
const newQuantity = existingItem.quantity + addItemDto.quantity;
|
const newQuantity = existingItem.quantity + addItemDto.quantity;
|
||||||
|
|
||||||
// Check stock for new total quantity
|
this.validateStock(food, newQuantity);
|
||||||
if (availableStock < newQuantity) {
|
|
||||||
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * newQuantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * newQuantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
cart.items[existingItemIndex] = {
|
cart.items[existingItemIndex] = {
|
||||||
...existingItem,
|
...existingItem,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
totalPrice: itemFinalPrice,
|
totalPrice: this.calculateItemTotalPrice(food, newQuantity),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Create new item
|
cart.items.push(this.createCartItem(food, addItemDto.quantity));
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * addItemDto.quantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * addItemDto.quantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
const cartItem: CartItem = {
|
|
||||||
foodId: food.id,
|
|
||||||
foodTitle: food.title,
|
|
||||||
quantity: addItemDto.quantity,
|
|
||||||
price: itemPrice,
|
|
||||||
discount: itemDiscount,
|
|
||||||
totalPrice: itemFinalPrice,
|
|
||||||
};
|
|
||||||
|
|
||||||
cart.items.push(cartItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate cart totals once after all items are added
|
|
||||||
await this.recalculateCartTotals(cart);
|
|
||||||
await this.saveCart(cart);
|
|
||||||
|
|
||||||
return cart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update item quantity in cart
|
|
||||||
*/
|
|
||||||
async updateItemQuantity(
|
|
||||||
userId: string,
|
|
||||||
restaurantId: string,
|
|
||||||
foodId: string,
|
|
||||||
updateItemDto: UpdateItemQuantityDto,
|
|
||||||
): Promise<Cart> {
|
|
||||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
||||||
|
|
||||||
const itemIndex = cart.items.findIndex(item => item.foodId === foodId);
|
|
||||||
if (itemIndex < 0) {
|
|
||||||
throw new NotFoundException(`Item with food ID ${foodId} not found in cart`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = cart.items[itemIndex];
|
|
||||||
|
|
||||||
// Find food to check stock
|
|
||||||
const food = await this.em.findOne(Food, { id: item.foodId }, { populate: ['restaurant', 'inventory'] });
|
|
||||||
if (!food) {
|
|
||||||
throw new NotFoundException(`Food with ID ${item.foodId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check stock availability
|
|
||||||
const availableStock = food.inventory?.availableStock ?? 0;
|
|
||||||
if (availableStock < updateItemDto.quantity) {
|
|
||||||
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update quantity
|
|
||||||
const itemPrice = food.price || 0;
|
|
||||||
const itemDiscount = food.discount || 0;
|
|
||||||
const itemTotalPrice = itemPrice * updateItemDto.quantity;
|
|
||||||
const itemTotalDiscount = itemDiscount * updateItemDto.quantity;
|
|
||||||
const itemFinalPrice = itemTotalPrice - itemTotalDiscount;
|
|
||||||
|
|
||||||
cart.items[itemIndex] = {
|
|
||||||
...item,
|
|
||||||
quantity: updateItemDto.quantity,
|
|
||||||
totalPrice: itemFinalPrice,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Recalculate cart totals
|
|
||||||
await this.recalculateCartTotals(cart);
|
await this.recalculateCartTotals(cart);
|
||||||
await this.saveCart(cart);
|
await this.saveCart(cart);
|
||||||
|
|
||||||
@@ -366,18 +205,17 @@ export class CartService {
|
|||||||
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
|
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
|
||||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||||
|
|
||||||
// Calculate current order amount (subtotal after item discounts) for coupon validation
|
|
||||||
// const currentSubTotal = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
||||||
// const currentItemsDiscount = cart.items.reduce((sum, item) => sum + item.discount * item.quantity, 0);
|
|
||||||
// const orderAmount = currentSubTotal - currentItemsDiscount;
|
|
||||||
|
|
||||||
const orderAmount = cart.subTotal - cart.itemsDiscount;
|
const orderAmount = cart.subTotal - cart.itemsDiscount;
|
||||||
|
const user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||||
|
}
|
||||||
// check if coupon is valid and belong to the restaurant
|
// check if coupon is valid and belong to the restaurant
|
||||||
const { valid, coupon, message } = await this.couponService.validateCoupon(
|
const { valid, coupon, message } = await this.couponService.validateCoupon(
|
||||||
applyCouponDto.code,
|
applyCouponDto.code,
|
||||||
restaurantId,
|
restaurantId,
|
||||||
Number(orderAmount),
|
Number(orderAmount),
|
||||||
|
user.phone,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
@@ -388,20 +226,6 @@ export class CartService {
|
|||||||
throw new BadRequestException('Coupon not found');
|
throw new BadRequestException('Coupon not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate user phone if coupon is restricted to a specific user
|
|
||||||
if (coupon.userPhone) {
|
|
||||||
const user = await this.em.findOne(User, { id: userId });
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.phone !== coupon.userPhone) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'This coupon is only available for a specific user and cannot be applied to your account.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
|
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
|
||||||
if (coupon.maxUsesPerUser) {
|
if (coupon.maxUsesPerUser) {
|
||||||
// Use native query for JSONB field access with proper parameter binding
|
// Use native query for JSONB field access with proper parameter binding
|
||||||
@@ -510,7 +334,14 @@ export class CartService {
|
|||||||
throw new BadRequestException('Address does not belong to the current user');
|
throw new BadRequestException('Address does not belong to the current user');
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.addressId = setAddressDto.addressId;
|
cart.userAddress = {
|
||||||
|
address: address.address,
|
||||||
|
city: address.city,
|
||||||
|
province: address.province || '',
|
||||||
|
postalCode: address.postalCode || '',
|
||||||
|
fullName: address.user.firstName + ' ' + address.user.lastName || '',
|
||||||
|
phone: address.user.phone,
|
||||||
|
};
|
||||||
cart.updatedAt = new Date().toISOString();
|
cart.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
await this.saveCart(cart);
|
await this.saveCart(cart);
|
||||||
@@ -618,18 +449,20 @@ export class CartService {
|
|||||||
async setTableNumber(userId: string, restaurantId: string, setTableNumberDto: SetTableNumberDto): Promise<Cart> {
|
async setTableNumber(userId: string, restaurantId: string, setTableNumberDto: SetTableNumberDto): Promise<Cart> {
|
||||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||||
|
|
||||||
// If delivery method is set, validate that table number is required for DineIn
|
if (!cart.deliveryMethodId) {
|
||||||
if (cart.deliveryMethodId) {
|
throw new BadRequestException('Delivery method must be set before setting table number');
|
||||||
const deliveryMethod = await this.em.findOne(Delivery, {
|
}
|
||||||
id: cart.deliveryMethodId,
|
const deliveryMethod = await this.em.findOne(Delivery, {
|
||||||
restaurant: { id: restaurantId },
|
id: cart.deliveryMethodId,
|
||||||
});
|
restaurant: { id: restaurantId },
|
||||||
|
});
|
||||||
if (deliveryMethod && deliveryMethod.method === DeliveryMethodEnum.DineIn) {
|
if (!deliveryMethod) {
|
||||||
if (!setTableNumberDto.tableNumber || setTableNumberDto.tableNumber.trim() === '') {
|
throw new NotFoundException(
|
||||||
throw new BadRequestException('Table number is required when delivery method is DineIn');
|
`Delivery method with ID ${cart.deliveryMethodId} not found for restaurant ${restaurantId}`,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
if (deliveryMethod.method !== DeliveryMethodEnum.DineIn) {
|
||||||
|
throw new BadRequestException('Delivery method must be DineIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.tableNumber = setTableNumberDto.tableNumber;
|
cart.tableNumber = setTableNumberDto.tableNumber;
|
||||||
@@ -640,6 +473,49 @@ export class CartService {
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set car delivery for cart
|
||||||
|
*/
|
||||||
|
async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise<Cart> {
|
||||||
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||||
|
|
||||||
|
if (!cart.deliveryMethodId)
|
||||||
|
throw new BadRequestException('Delivery method must be set before setting car delivery');
|
||||||
|
const deliveryMethod = await this.em.findOne(Delivery, {
|
||||||
|
id: cart.deliveryMethodId,
|
||||||
|
restaurant: { id: restaurantId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deliveryMethod) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Delivery method with ID ${cart.deliveryMethodId} not found for restaurant ${restaurantId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (deliveryMethod.method !== DeliveryMethodEnum.DeliveryCar) {
|
||||||
|
throw new BadRequestException('Delivery method must be DeliveryCar');
|
||||||
|
}
|
||||||
|
const user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cart.carAddress = {
|
||||||
|
carModel: setCarDeliveryDto.carModel,
|
||||||
|
carColor: setCarDeliveryDto.carColor,
|
||||||
|
plateNumber: setCarDeliveryDto.plateNumber,
|
||||||
|
address: setCarDeliveryDto.address,
|
||||||
|
latitude: setCarDeliveryDto.latitude,
|
||||||
|
longitude: setCarDeliveryDto.longitude,
|
||||||
|
fullName: setCarDeliveryDto.fullName,
|
||||||
|
phone: user.phone,
|
||||||
|
};
|
||||||
|
cart.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await this.saveCart(cart);
|
||||||
|
|
||||||
|
return cart;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate cart totals (including coupon discount and tax)
|
* Recalculate cart totals (including coupon discount and tax)
|
||||||
*/
|
*/
|
||||||
@@ -778,7 +654,7 @@ export class CartService {
|
|||||||
* Get cart by restaurant for a user
|
* Get cart by restaurant for a user
|
||||||
*/
|
*/
|
||||||
private async getCartByRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
|
private async getCartByRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
|
||||||
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
const cacheKey = this.getCacheKey(userId, restaurantId);
|
||||||
const cachedCart = await this.cacheService.get<string>(cacheKey);
|
const cachedCart = await this.cacheService.get<string>(cacheKey);
|
||||||
|
|
||||||
if (cachedCart) {
|
if (cachedCart) {
|
||||||
@@ -799,12 +675,81 @@ export class CartService {
|
|||||||
* Save cart to cache
|
* Save cart to cache
|
||||||
*/
|
*/
|
||||||
private async saveCart(cart: Cart): Promise<void> {
|
private async saveCart(cart: Cart): Promise<void> {
|
||||||
const cacheKey = `${this.CART_KEY_PREFIX}:${cart.userId}:${cart.restaurantId}`;
|
const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId);
|
||||||
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
|
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cart from cache
|
||||||
|
*/
|
||||||
async clearCart(userId: string, restaurantId: string): Promise<void> {
|
async clearCart(userId: string, restaurantId: string): Promise<void> {
|
||||||
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
const cacheKey = this.getCacheKey(userId, restaurantId);
|
||||||
return this.cacheService.del(cacheKey);
|
return this.cacheService.del(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cache key for cart
|
||||||
|
*/
|
||||||
|
private getCacheKey(userId: string, restaurantId: string): string {
|
||||||
|
return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate food exists, belongs to restaurant, has inventory, and check stock
|
||||||
|
*/
|
||||||
|
private async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
|
||||||
|
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
|
||||||
|
if (!food) {
|
||||||
|
throw new NotFoundException(`Food with ID ${foodId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (food.restaurant.id !== restaurantId) {
|
||||||
|
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!food.inventory) {
|
||||||
|
throw new BadRequestException(`The food ${food.title || food.id} does not have inventory`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateStock(food, quantity);
|
||||||
|
return food;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate stock availability for food
|
||||||
|
*/
|
||||||
|
private validateStock(food: Food, quantity: number): void {
|
||||||
|
const availableStock = food.inventory!.availableStock;
|
||||||
|
if (availableStock < quantity) {
|
||||||
|
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total price for a cart item
|
||||||
|
*/
|
||||||
|
private calculateItemTotalPrice(food: Food, quantity: number): number {
|
||||||
|
const itemPrice = food.price || 0;
|
||||||
|
const itemDiscount = food.discount || 0;
|
||||||
|
const itemTotalPrice = itemPrice * quantity;
|
||||||
|
const itemTotalDiscount = itemDiscount * quantity;
|
||||||
|
return itemTotalPrice - itemTotalDiscount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new cart item from food
|
||||||
|
*/
|
||||||
|
private createCartItem(food: Food, quantity: number): CartItem {
|
||||||
|
const itemPrice = food.price || 0;
|
||||||
|
const itemDiscount = food.discount || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
foodId: food.id,
|
||||||
|
foodTitle: food.title,
|
||||||
|
quantity,
|
||||||
|
price: itemPrice,
|
||||||
|
discount: itemDiscount,
|
||||||
|
totalPrice: this.calculateItemTotalPrice(food, quantity),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export class CouponService {
|
|||||||
code: string,
|
code: string,
|
||||||
restId: string,
|
restId: string,
|
||||||
orderAmount: number,
|
orderAmount: number,
|
||||||
|
userPhone: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
coupon?: Coupon;
|
coupon?: Coupon;
|
||||||
@@ -179,6 +180,14 @@ export class CouponService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coupon.userPhone) {
|
||||||
|
if (userPhone !== coupon.userPhone) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: CouponMessage.COUPON_RESTRICTED_TO_USER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
// Check if coupon is active
|
// Check if coupon is active
|
||||||
if (!coupon.isActive) {
|
if (!coupon.isActive) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import { PaymentStatusEnum } from '../../payments/interface/payment';
|
|||||||
import { OrderCouponDetail, 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 { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
||||||
import { OrderItem } from './order-item.entity';
|
import { OrderItem } from './order-item.entity';
|
||||||
import { Delivery } from '../../delivery/entities/delivery.entity';
|
import { Delivery } from '../../delivery/entities/delivery.entity';
|
||||||
|
import { OrderUserAddress, OrderCarAddress } from '../interface/order-status';
|
||||||
|
|
||||||
@Entity({ tableName: 'orders' })
|
@Entity({ tableName: 'orders' })
|
||||||
@Unique({ properties: ['restaurant', 'orderNumber'] })
|
@Unique({ properties: ['restaurant', 'orderNumber'] })
|
||||||
@@ -45,16 +45,10 @@ export class Order extends BaseEntity {
|
|||||||
items = new Collection<OrderItem>(this);
|
items = new Collection<OrderItem>(this);
|
||||||
|
|
||||||
@Property({ type: 'json', nullable: true })
|
@Property({ type: 'json', nullable: true })
|
||||||
address?: {
|
userAddress?: OrderUserAddress | null;
|
||||||
address: string;
|
// for car delivery
|
||||||
latitude: number;
|
@Property({ type: 'json', nullable: true })
|
||||||
longitude: number;
|
carAddress?: OrderCarAddress | null;
|
||||||
city: string;
|
|
||||||
province: string;
|
|
||||||
postalCode: string;
|
|
||||||
fullName: string;
|
|
||||||
phone: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@ManyToOne(() => PaymentMethod)
|
@ManyToOne(() => PaymentMethod)
|
||||||
paymentMethod: PaymentMethod;
|
paymentMethod: PaymentMethod;
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import type { CouponType } from 'src/modules/coupons/interface/coupon';
|
import type { CouponType } from 'src/modules/coupons/interface/coupon';
|
||||||
|
|
||||||
// export enum OrderStatus {
|
export interface OrderUserAddress {
|
||||||
// // Initial status
|
address?: string;
|
||||||
// Pending = 'pending',
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
postalCode?: string;
|
||||||
|
fullName: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
// // Cancellation statuses
|
export interface OrderCarAddress {
|
||||||
// CancelledBySystem = 'cancelledBySystem',
|
carModel: string;
|
||||||
// CancelledByUser = 'cancelledByUser',
|
carColor: string;
|
||||||
// RejectedByRestaurant = 'rejectedByRestaurant',
|
plateNumber: string;
|
||||||
|
address?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
fullName: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
// // Active processing statuses
|
|
||||||
// Confirmed = 'confirmed',
|
|
||||||
// Preparing = 'preparing',
|
|
||||||
|
|
||||||
// // Ready for pickup/delivery statuses
|
|
||||||
// ReadyForCustomerPickup = 'readyForCustomerPickup',
|
|
||||||
// ReadyForDineIn = 'readyForDineIn',
|
|
||||||
// ReadyForDeliveryCar = 'readyForDeliveryCar',
|
|
||||||
// ReadyForDeliveryCourier = 'readyForDeliveryCourier',
|
|
||||||
|
|
||||||
// Shipping = 'shipping',
|
|
||||||
// // Final status
|
|
||||||
// Delivered = 'delivered',
|
|
||||||
// }
|
|
||||||
export enum OrderStatus {
|
export enum OrderStatus {
|
||||||
NEW = 'new',
|
NEW = 'new',
|
||||||
PENDING_PAYMENT = 'pendingPayment',
|
PENDING_PAYMENT = 'pendingPayment',
|
||||||
@@ -32,7 +31,7 @@ export enum OrderStatus {
|
|||||||
READY = 'ready',
|
READY = 'ready',
|
||||||
SHIPPED = 'shipped',
|
SHIPPED = 'shipped',
|
||||||
COMPLETED = 'completed',
|
COMPLETED = 'completed',
|
||||||
|
|
||||||
CANCELED = 'canceled',
|
CANCELED = 'canceled',
|
||||||
FAILED = 'failed',
|
FAILED = 'failed',
|
||||||
REFUNDED = 'refunded',
|
REFUNDED = 'refunded',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { OrderItem } from '../entities/order-item.entity';
|
|||||||
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 { Food } from '../../foods/entities/food.entity';
|
import { Food } from '../../foods/entities/food.entity';
|
||||||
import { UserAddress } from '../../users/entities/user-address.entity';
|
|
||||||
import { CartService } from '../../cart/providers/cart.service';
|
import { CartService } from '../../cart/providers/cart.service';
|
||||||
import { OrderStatus } from '../interface/order-status';
|
import { OrderStatus } from '../interface/order-status';
|
||||||
import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment';
|
import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment';
|
||||||
@@ -20,6 +19,7 @@ import { OrderRepository } from '../repositories/order.repository';
|
|||||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { OrderUserAddress, OrderCarAddress } from '../interface/order-status';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersService {
|
export class OrdersService {
|
||||||
@@ -68,7 +68,8 @@ export class OrdersService {
|
|||||||
user: validationResult.user,
|
user: validationResult.user,
|
||||||
restaurant: validationResult.restaurant,
|
restaurant: validationResult.restaurant,
|
||||||
deliveryMethod: validationResult.delivery,
|
deliveryMethod: validationResult.delivery,
|
||||||
address: validationResult.address,
|
userAddress: validationResult.userAddress,
|
||||||
|
carAddress: validationResult.carAddress,
|
||||||
paymentMethod: validationResult.paymentMethod,
|
paymentMethod: validationResult.paymentMethod,
|
||||||
couponDiscount: cart.couponDiscount || 0,
|
couponDiscount: cart.couponDiscount || 0,
|
||||||
itemsDiscount: cart.itemsDiscount || 0,
|
itemsDiscount: cart.itemsDiscount || 0,
|
||||||
@@ -133,7 +134,8 @@ export class OrdersService {
|
|||||||
user: User;
|
user: User;
|
||||||
restaurant: Restaurant;
|
restaurant: Restaurant;
|
||||||
delivery: Delivery;
|
delivery: Delivery;
|
||||||
address: UserAddress | null;
|
userAddress: OrderUserAddress | null;
|
||||||
|
carAddress: OrderCarAddress | null;
|
||||||
paymentMethod: PaymentMethod;
|
paymentMethod: PaymentMethod;
|
||||||
orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }>;
|
orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }>;
|
||||||
}> {
|
}> {
|
||||||
@@ -185,19 +187,12 @@ export class OrdersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.addressId) {
|
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) {
|
||||||
throw new BadRequestException('Address is required. Please set a delivery address before creating an order.');
|
throw new BadRequestException('Address is required. Please set a delivery address before creating an order.');
|
||||||
}
|
}
|
||||||
let address: UserAddress | null = null;
|
|
||||||
if (cart.addressId) {
|
if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) {
|
||||||
address = await this.em.findOne(UserAddress, { id: cart.addressId }, { populate: ['user'] });
|
throw new BadRequestException('Car address is required. Please set a car address before creating an order.');
|
||||||
if (!address) {
|
|
||||||
throw new NotFoundException('Address not found');
|
|
||||||
}
|
|
||||||
// Verify address belongs to the user
|
|
||||||
if (address.user.id !== userId) {
|
|
||||||
throw new BadRequestException('Address does not belong to the current user');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethod = await this.em.findOne(
|
const paymentMethod = await this.em.findOne(
|
||||||
@@ -256,8 +251,9 @@ export class OrdersService {
|
|||||||
user,
|
user,
|
||||||
restaurant,
|
restaurant,
|
||||||
delivery,
|
delivery,
|
||||||
address,
|
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
|
userAddress: cart?.userAddress ?? null,
|
||||||
|
carAddress: cart?.carAddress ?? null,
|
||||||
orderItemsData,
|
orderItemsData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -298,7 +294,18 @@ export class OrdersService {
|
|||||||
async findOne(id: string, restId: string): Promise<Order> {
|
async findOne(id: string, restId: string): Promise<Order> {
|
||||||
const order = await this.orderRepository.findOne(
|
const order = await this.orderRepository.findOne(
|
||||||
{ id, restaurant: { id: restId } },
|
{ id, restaurant: { id: restId } },
|
||||||
{ populate: ['user', 'restaurant', 'deliveryMethod', 'address', 'paymentMethod', 'items', 'items.food'] },
|
{
|
||||||
|
populate: [
|
||||||
|
'user',
|
||||||
|
'restaurant',
|
||||||
|
'deliveryMethod',
|
||||||
|
'userAddress',
|
||||||
|
'carAddress',
|
||||||
|
'paymentMethod',
|
||||||
|
'items',
|
||||||
|
'items.food',
|
||||||
|
],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new NotFoundException('Order not found');
|
throw new NotFoundException('Order not found');
|
||||||
|
|||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
import { HttpService } from "@nestjs/axios";
|
||||||
|
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { catchError, firstValueFrom, throwError } from "rxjs";
|
||||||
|
|
||||||
|
import { PaymentMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { IZarinpalConfig } from "../../../configs/zarinpal.config";
|
||||||
|
import { ZARINPAL_CONFIG } from "../constants";
|
||||||
|
import { GatewayEnum } from "../enums/gateway.enum";
|
||||||
|
import {
|
||||||
|
IPaymentGateway,
|
||||||
|
IPaymentVerifyParams,
|
||||||
|
IProcessPaymentParams,
|
||||||
|
ZarinPalPGNewArgs,
|
||||||
|
ZarinPalPGNewRequestData,
|
||||||
|
ZarinPalPGVerifyData,
|
||||||
|
} from "../interfaces/IPayment";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ZarinpalGateway implements IPaymentGateway {
|
||||||
|
private readonly IPG_TYPE = "payment";
|
||||||
|
private readonly logger = new Logger(ZarinpalGateway.name);
|
||||||
|
private readonly gatewayApiUrl: string;
|
||||||
|
private readonly requestHeader: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json" };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
) {
|
||||||
|
this.gatewayApiUrl = `https://${this.IPG_TYPE}.zarinpal.com`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPayment(processParams: IProcessPaymentParams) {
|
||||||
|
try {
|
||||||
|
const purchaseData: ZarinPalPGNewArgs = {
|
||||||
|
merchant_id: this.config.merchantId,
|
||||||
|
amount: processParams.amount,
|
||||||
|
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||||
|
description: processParams.description,
|
||||||
|
currency: "IRT",
|
||||||
|
metadata: { email: processParams.email, mobile: processParams.mobile },
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log(`Processing payment request:`, {
|
||||||
|
merchant_id: this.config.merchantId,
|
||||||
|
amount: processParams.amount,
|
||||||
|
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||||
|
description: processParams.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await firstValueFrom(
|
||||||
|
this.httpService
|
||||||
|
.post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, {
|
||||||
|
headers: this.requestHeader,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError((err: AxiosError) => {
|
||||||
|
this.logger.error(`Payment request failed: ${err.message}`, err.stack);
|
||||||
|
return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for errors in response
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
this.logger.error(`Zarinpal payment request error:`, data.errors);
|
||||||
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response data
|
||||||
|
if (!data.data?.authority) {
|
||||||
|
this.logger.error(`Invalid response from Zarinpal:`, data);
|
||||||
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Payment request successful - Authority: ${data.data.authority}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`,
|
||||||
|
message: data.data.message,
|
||||||
|
reference: data.data.authority,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Payment processing error:`, error);
|
||||||
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
||||||
|
const verifyData = {
|
||||||
|
merchant_id: this.config.merchantId,
|
||||||
|
amount: Number(verifyParams.amount),
|
||||||
|
authority: verifyParams.reference,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log(`Verifying payment:`, {
|
||||||
|
merchant_id: this.config.merchantId,
|
||||||
|
amount: Number(verifyParams.amount),
|
||||||
|
authority: verifyParams.reference,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await firstValueFrom(
|
||||||
|
this.httpService
|
||||||
|
.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/pg/v4/payment/verify.json`, verifyData, {
|
||||||
|
headers: this.requestHeader,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError((err: AxiosError) => {
|
||||||
|
this.logger.error(`Verification request failed: ${err.message}`, err.stack);
|
||||||
|
|
||||||
|
// If we have error response data from Zarinpal, extract it
|
||||||
|
if (err.response?.data) {
|
||||||
|
const errorData = err.response.data as ZarinPalPGVerifyData;
|
||||||
|
this.logger.error(`Zarinpal error response:`, errorData);
|
||||||
|
|
||||||
|
// Return the Zarinpal error response if it has the expected structure
|
||||||
|
if (errorData.errors) {
|
||||||
|
return throwError(() => errorData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the verification result
|
||||||
|
if (data.data) {
|
||||||
|
this.logger.log(`Verification response - Code: ${data.data.code}, RefID: ${data.data.ref_id || "N/A"}`);
|
||||||
|
} else if (data.errors && data.errors.length > 0) {
|
||||||
|
this.logger.warn(`Verification error - Code: ${data.errors[0].code}, Message: ${data.errors[0].message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the complete Zarinpal response - let the service handle the business logic
|
||||||
|
return {
|
||||||
|
code: data.data?.code || data.errors?.[0]?.code || -1,
|
||||||
|
message: data.data?.message || data.errors?.[0]?.message || "Unknown error",
|
||||||
|
ref_id: data.data?.ref_id || 0,
|
||||||
|
card_hash: data.data?.card_hash || "",
|
||||||
|
card_pan: data.data?.card_pan || "",
|
||||||
|
fee_type: data.data?.fee_type || "",
|
||||||
|
fee: data.data?.fee || 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// If it's a Zarinpal error response, extract the error code and return it
|
||||||
|
if (error && typeof error === "object" && "errors" in error) {
|
||||||
|
const zarinpalError = error as ZarinPalPGVerifyData;
|
||||||
|
const firstError = zarinpalError.errors[0];
|
||||||
|
this.logger.warn(`Zarinpal verification error - Code: ${firstError.code}, Message: ${firstError.message}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: firstError.code,
|
||||||
|
message: firstError.message,
|
||||||
|
ref_id: 0,
|
||||||
|
card_hash: "",
|
||||||
|
card_pan: "",
|
||||||
|
fee_type: "",
|
||||||
|
fee: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Payment verification error:`, error);
|
||||||
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user