This commit is contained in:
@@ -11,7 +11,7 @@ import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/p
|
||||
import { Cart } from '../../cart/interfaces/cart.interface';
|
||||
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
||||
import { PaymentsService } from '../../payments/services/payments.service';
|
||||
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
|
||||
import { DeliveryFeeTypeEnum, DeliveryMethodEnum } from '../../delivery/interface/delivery';
|
||||
import { Delivery } from '../../delivery/entities/delivery.entity';
|
||||
import { OrderRepository } from '../repositories/order.repository';
|
||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||
@@ -23,6 +23,8 @@ import { StatusTransitionRef } from '../interface/order.interface';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
|
||||
import { OrderMessage } from 'src/common/enums/message.enum';
|
||||
import { AdminCreateOrderDto } from '../dto/admin-create-order.dto';
|
||||
import { normalizePhone } from 'src/modules/utils/phone.util';
|
||||
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
|
||||
|
||||
type ValidatedCartForOrder = {
|
||||
@@ -140,6 +142,98 @@ export class OrdersService {
|
||||
return { paymentUrl, order };
|
||||
}
|
||||
|
||||
async createOrderForUserByPhone(restaurantId: string, dto: AdminCreateOrderDto): Promise<Order> {
|
||||
const normalizedPhone = normalizePhone(dto.userPhone);
|
||||
let user = await this.em.findOne(User, { phone: normalizedPhone });
|
||||
if (!user) {
|
||||
user = this.em.create(User, {
|
||||
phone: normalizedPhone,
|
||||
firstName: dto.firstName?.trim() || normalizedPhone,
|
||||
lastName: dto.lastName?.trim(),
|
||||
});
|
||||
await this.em.persistAndFlush(user);
|
||||
this.logger.debug(`Auto-created user ${user.id} with phone ${normalizedPhone} via admin order`);
|
||||
}
|
||||
|
||||
const [restaurant, delivery] = await Promise.all([
|
||||
this.getRestaurantOrFail(restaurantId),
|
||||
this.getDeliveryOrFail(dto.deliveryMethodId),
|
||||
]);
|
||||
|
||||
const paymentMethod = await this.getPaymentMethodOrFail(dto.paymentMethodId, restaurantId);
|
||||
this.assertPaymentMethodEnabled(paymentMethod);
|
||||
|
||||
this.assertAdminDeliveryRequirements(dto, delivery);
|
||||
|
||||
const orderItemsData = await this.buildOrderItemsDataFromDto(dto.items, restaurantId);
|
||||
|
||||
const subTotal = orderItemsData.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
|
||||
const itemsDiscount = orderItemsData.reduce((sum, item) => sum + item.discount * item.quantity, 0);
|
||||
const deliveryFee = delivery.deliveryFeeType === DeliveryFeeTypeEnum.FIXED ? Number(delivery.deliveryFee) : 0;
|
||||
const totalDiscount = itemsDiscount;
|
||||
const total = subTotal - totalDiscount + deliveryFee;
|
||||
const totalItems = orderItemsData.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
const order = await this.em.transactional(async em => {
|
||||
const order = em.create(Order, {
|
||||
user,
|
||||
restaurant,
|
||||
deliveryMethod: delivery,
|
||||
userAddress: delivery.method === DeliveryMethodEnum.DeliveryCourier ? (dto.userAddress ?? null) : null,
|
||||
carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (dto.carAddress ?? null) : null,
|
||||
paymentMethod,
|
||||
couponDiscount: 0,
|
||||
couponDetail: null,
|
||||
itemsDiscount,
|
||||
totalDiscount,
|
||||
subTotal,
|
||||
tax: 0,
|
||||
deliveryFee,
|
||||
total,
|
||||
totalItems,
|
||||
description: dto.description,
|
||||
tableNumber: dto.tableNumber,
|
||||
status: OrderStatus.PENDING_PAYMENT,
|
||||
history: [{ status: OrderStatus.PENDING_PAYMENT, changedAt: new Date() }],
|
||||
});
|
||||
|
||||
em.persist(order);
|
||||
|
||||
for (const itemData of orderItemsData) {
|
||||
const { food, quantity, unitPrice, discount } = itemData;
|
||||
this.assertFoodHasSufficientStock(food, quantity);
|
||||
const totalPrice = (unitPrice - discount) * quantity;
|
||||
const orderItem = em.create(OrderItem, { order, food, quantity, unitPrice, discount, totalPrice });
|
||||
em.persist(orderItem);
|
||||
}
|
||||
|
||||
const payment = em.create(Payment, {
|
||||
order,
|
||||
amount: order.total,
|
||||
status: PaymentStatusEnum.Pending,
|
||||
method: order.paymentMethod.method,
|
||||
gateway: order.paymentMethod.gateway ?? null,
|
||||
});
|
||||
em.persist(payment);
|
||||
|
||||
const bulkReserveFoodDto: BulkReserveFoodDto = {
|
||||
items: orderItemsData.map(item => ({ foodId: item.food.id, quantity: item.quantity })),
|
||||
};
|
||||
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
|
||||
await em.flush();
|
||||
|
||||
this.logger.debug(`Admin created order ${order.id} for user ${user.id} (phone: ${normalizedPhone}, restaurant: ${restaurantId})`);
|
||||
return order;
|
||||
});
|
||||
|
||||
this.eventEmitter.emit(
|
||||
OrderCreatedEvent.name,
|
||||
new OrderCreatedEvent(order.id, restaurantId, String(order?.orderNumber) || '', order.total),
|
||||
);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates cart and prepares all required data for order creation
|
||||
*/
|
||||
@@ -453,6 +547,49 @@ export class OrdersService {
|
||||
}
|
||||
}
|
||||
|
||||
private async buildOrderItemsDataFromDto(
|
||||
items: { foodId: string; quantity: number }[],
|
||||
restaurantId: string,
|
||||
): Promise<OrderItemData[]> {
|
||||
const orderItemsData: OrderItemData[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const food = await this.em.findOne(Food, { id: item.foodId }, { populate: ['restaurant', 'inventory'] });
|
||||
if (!food) throw new NotFoundException(OrderMessage.FOOD_NOT_FOUND);
|
||||
|
||||
if (food.restaurant.id !== restaurantId) {
|
||||
throw new BadRequestException(OrderMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
|
||||
}
|
||||
|
||||
this.assertFoodHasSufficientStock(food, item.quantity);
|
||||
|
||||
orderItemsData.push({
|
||||
food,
|
||||
quantity: item.quantity,
|
||||
unitPrice: food.price || 0,
|
||||
discount: food.discount || 0,
|
||||
});
|
||||
}
|
||||
|
||||
return orderItemsData;
|
||||
}
|
||||
|
||||
private assertAdminDeliveryRequirements(dto: AdminCreateOrderDto, delivery: Delivery) {
|
||||
if (delivery.method === DeliveryMethodEnum.DineIn) {
|
||||
if (!dto.tableNumber || dto.tableNumber.trim() === '') {
|
||||
throw new BadRequestException(OrderMessage.TABLE_NUMBER_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !dto.userAddress) {
|
||||
throw new BadRequestException(OrderMessage.ADDRESS_REQUIRED);
|
||||
}
|
||||
|
||||
if (delivery.method === DeliveryMethodEnum.DeliveryCar && !dto.carAddress) {
|
||||
throw new BadRequestException(OrderMessage.CAR_ADDRESS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getStats(restId: string) {
|
||||
return this.em.transactional(async em => {
|
||||
|
||||
Reference in New Issue
Block a user