order by admin
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-03 16:56:40 +03:30
parent 4f8cf60174
commit 17869470ab
6 changed files with 272 additions and 2 deletions
+138 -1
View File
@@ -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 => {