create order
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
export class CreateOrderDto {}
|
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { OrdersService } from './orders.service';
|
import { OrdersService } from './orders.service';
|
||||||
import { CreateOrderDto } from './dto/create-order.dto';
|
|
||||||
import { UpdateOrderDto } from './dto/update-order.dto';
|
import { UpdateOrderDto } from './dto/update-order.dto';
|
||||||
|
import { AuthGuard } from '../auth/guards/auth.guard';
|
||||||
|
import { UserId } from '../../common/decorators/user-id.decorator';
|
||||||
|
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||||
|
|
||||||
@Controller('orders')
|
@ApiTags('orders')
|
||||||
|
@Controller()
|
||||||
export class OrdersController {
|
export class OrdersController {
|
||||||
constructor(private readonly ordersService: OrdersService) {}
|
constructor(private readonly ordersService: OrdersService) {}
|
||||||
|
|
||||||
@Post()
|
@UseGuards(AuthGuard)
|
||||||
create(@Body() createOrderDto: CreateOrderDto) {
|
@ApiBearerAuth()
|
||||||
return this.ordersService.create(createOrderDto);
|
@Post('public/orders')
|
||||||
|
@ApiOperation({ summary: 'Create a new order from cart' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Order created successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Bad request - cart validation failed' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Cart, user, restaurant, address, or payment method not found' })
|
||||||
|
create(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||||
|
return this.ordersService.create(userId, restaurantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
import { OrdersService } from './orders.service';
|
import { OrdersService } from './orders.service';
|
||||||
import { OrdersController } from './orders.controller';
|
import { OrdersController } from './orders.controller';
|
||||||
|
import { Order } from './entities/order.entity';
|
||||||
|
import { OrderItem } from './entities/order-item.entity';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||||
|
import { Food } from '../foods/entities/food.entity';
|
||||||
|
import { UserAddress } from '../users/entities/user-address.entity';
|
||||||
|
import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity';
|
||||||
|
import { CartModule } from '../cart/cart.module';
|
||||||
|
import { UtilsModule } from '../utils/utils.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MikroOrmModule.forFeature([
|
||||||
|
Order,
|
||||||
|
OrderItem,
|
||||||
|
User,
|
||||||
|
Restaurant,
|
||||||
|
Food,
|
||||||
|
UserAddress,
|
||||||
|
RestaurantPaymentMethod,
|
||||||
|
]),
|
||||||
|
CartModule,
|
||||||
|
UtilsModule,
|
||||||
|
],
|
||||||
controllers: [OrdersController],
|
controllers: [OrdersController],
|
||||||
providers: [OrdersService],
|
providers: [OrdersService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,199 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { CreateOrderDto } from './dto/create-order.dto';
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
import { UpdateOrderDto } from './dto/update-order.dto';
|
import { Order } from './entities/order.entity';
|
||||||
|
import { OrderItem } from './entities/order-item.entity';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||||
|
import { Food } from '../foods/entities/food.entity';
|
||||||
|
import { UserAddress } from '../users/entities/user-address.entity';
|
||||||
|
import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity';
|
||||||
|
import { CartService } from '../cart/providers/cart.service';
|
||||||
|
import { CacheService } from '../utils/cache.service';
|
||||||
|
import { OrderStatus } from './interface/order-status';
|
||||||
|
import { PaymentStatus } from '../payments/interface/payment-status';
|
||||||
|
import { Cart } from '../cart/interfaces/cart.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersService {
|
export class OrdersService {
|
||||||
create(createOrderDto: CreateOrderDto) {
|
private readonly CART_KEY_PREFIX = 'cart';
|
||||||
return 'This action adds a new order';
|
|
||||||
|
constructor(
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
private readonly cartService: CartService,
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(userId: string, restaurantId: string): Promise<Order> {
|
||||||
|
// Get cart
|
||||||
|
const cart = await this.cartService.findOne(userId, restaurantId);
|
||||||
|
|
||||||
|
// Validate cart and prepare order data
|
||||||
|
const validationResult = await this.validateCartForOrder(userId, restaurantId, cart);
|
||||||
|
|
||||||
|
// Create order within a transaction
|
||||||
|
return await this.em.transactional(async em => {
|
||||||
|
// Create order entity
|
||||||
|
const order = em.create(Order, {
|
||||||
|
user: validationResult.user,
|
||||||
|
restaurant: validationResult.restaurant,
|
||||||
|
address: validationResult.address,
|
||||||
|
paymentMethod: validationResult.paymentMethod,
|
||||||
|
couponDiscount: cart.couponDiscount || 0,
|
||||||
|
itemsDiscount: cart.itemsDiscount || 0,
|
||||||
|
totalDiscount: cart.totalDiscount || 0,
|
||||||
|
subTotal: cart.subTotal || 0,
|
||||||
|
tax: cart.tax || 0,
|
||||||
|
deliveryFee: cart.deliveryFee || 0,
|
||||||
|
total: cart.total || 0,
|
||||||
|
totalItems: cart.totalItems || 0,
|
||||||
|
status: OrderStatus.Pending,
|
||||||
|
paymentStatus: PaymentStatus.Pending,
|
||||||
|
});
|
||||||
|
|
||||||
|
em.persist(order);
|
||||||
|
|
||||||
|
// Create order items and update stock
|
||||||
|
for (const itemData of validationResult.orderItemsData) {
|
||||||
|
const { food, quantity, unitPrice, discount } = itemData;
|
||||||
|
const totalPrice = (unitPrice - discount) * quantity;
|
||||||
|
|
||||||
|
const orderItem = em.create(OrderItem, {
|
||||||
|
order,
|
||||||
|
food,
|
||||||
|
quantity,
|
||||||
|
unitPrice,
|
||||||
|
discount,
|
||||||
|
totalPrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
em.persist(orderItem);
|
||||||
|
|
||||||
|
// Update food stock
|
||||||
|
food.stock -= quantity;
|
||||||
|
em.persist(food);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush all changes
|
||||||
|
await em.flush();
|
||||||
|
|
||||||
|
// Clear cart from cache after successful order creation
|
||||||
|
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
||||||
|
await this.cacheService.del(cacheKey);
|
||||||
|
|
||||||
|
return order;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates cart and prepares all required data for order creation
|
||||||
|
*/
|
||||||
|
private async validateCartForOrder(
|
||||||
|
userId: string,
|
||||||
|
restaurantId: string,
|
||||||
|
cart: Cart,
|
||||||
|
): Promise<{
|
||||||
|
user: User;
|
||||||
|
restaurant: Restaurant;
|
||||||
|
address: UserAddress;
|
||||||
|
paymentMethod: RestaurantPaymentMethod;
|
||||||
|
orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }>;
|
||||||
|
}> {
|
||||||
|
// Validate cart has items
|
||||||
|
if (!cart.items || cart.items.length === 0) {
|
||||||
|
throw new BadRequestException('Cart is empty. Add items to cart before creating an order.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate address is set
|
||||||
|
if (!cart.addressId) {
|
||||||
|
throw new BadRequestException('Address is required. Please set a delivery address before creating an order.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate payment method is set
|
||||||
|
if (!cart.paymentMethodId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Payment method is required. Please set a payment method before creating an order.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and load entities
|
||||||
|
const user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||||
|
if (!restaurant) {
|
||||||
|
throw new NotFoundException('Restaurant not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = await this.em.findOne(UserAddress, { id: cart.addressId }, { populate: ['user'] });
|
||||||
|
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(
|
||||||
|
RestaurantPaymentMethod,
|
||||||
|
{
|
||||||
|
restaurant: { id: restaurantId },
|
||||||
|
paymentMethod: { id: cart.paymentMethodId },
|
||||||
|
},
|
||||||
|
{ populate: ['restaurant', 'paymentMethod'] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!paymentMethod) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Payment method with ID ${cart.paymentMethodId} not found for restaurant ${restaurantId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paymentMethod.isActive) {
|
||||||
|
throw new BadRequestException('Payment method is not active for this restaurant');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate stock and prepare order items data
|
||||||
|
const orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }> = [];
|
||||||
|
|
||||||
|
for (const cartItem of cart.items) {
|
||||||
|
const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant'] });
|
||||||
|
if (!food) {
|
||||||
|
throw new NotFoundException(`Food with ID ${cartItem.foodId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify food belongs to the restaurant
|
||||||
|
if (food.restaurant.id !== restaurantId) {
|
||||||
|
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final stock validation
|
||||||
|
if (food.stock < cartItem.quantity) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Insufficient stock for food ${food.title || food.id}. Available: ${food.stock}, Requested: ${cartItem.quantity}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitPrice = food.price || 0;
|
||||||
|
const discount = food.discount || 0;
|
||||||
|
|
||||||
|
orderItemsData.push({
|
||||||
|
food,
|
||||||
|
quantity: cartItem.quantity,
|
||||||
|
unitPrice,
|
||||||
|
discount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
restaurant,
|
||||||
|
address,
|
||||||
|
paymentMethod,
|
||||||
|
orderItemsData,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
findAll() {
|
||||||
@@ -16,7 +204,8 @@ export class OrdersService {
|
|||||||
return `This action returns a #${id} order`;
|
return `This action returns a #${id} order`;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updateOrderDto: UpdateOrderDto) {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
update(id: number, _updateOrderDto: unknown) {
|
||||||
return `This action updates a #${id} order`;
|
return `This action updates a #${id} order`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user