This commit is contained in:
@@ -70,10 +70,10 @@ export class OrdersController {
|
|||||||
@UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
@Permissions(Permission.MANAGE_ORDERS)
|
@Permissions(Permission.MANAGE_ORDERS)
|
||||||
@Post('admin/orders')
|
@Post('admin/orders')
|
||||||
@ApiOperation({ summary: 'Create an order for a user (by phone) from admin side' })
|
@ApiOperation({ summary: 'Create an order for a user from admin side' })
|
||||||
@ApiBody({ type: AdminCreateOrderDto })
|
@ApiBody({ type: AdminCreateOrderDto })
|
||||||
createOrderForUser(@RestId() restId: string, @Body() dto: AdminCreateOrderDto) {
|
createOrderForUser(@RestId() restId: string, @Body() dto: AdminCreateOrderDto) {
|
||||||
return this.ordersService.createOrderForUserByPhone(restId, dto);
|
return this.ordersService.createOrderForUser(restId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ArrayMinSize,
|
ArrayMinSize,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import type { OrderCarAddress, OrderUserAddress } from '../interface/order.interface';
|
import type { OrderCarAddress } from '../interface/order.interface';
|
||||||
|
|
||||||
export class AdminOrderItemDto {
|
export class AdminOrderItemDto {
|
||||||
@ApiProperty({ description: 'Food ID' })
|
@ApiProperty({ description: 'Food ID' })
|
||||||
@@ -25,20 +25,10 @@ export class AdminOrderItemDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AdminCreateOrderDto {
|
export class AdminCreateOrderDto {
|
||||||
@ApiProperty({ description: 'Phone number of the user to create the order for' })
|
@ApiProperty({ description: 'User ID to create the order for' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
userPhone: string;
|
userId: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'First name — used when creating a new user' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
firstName?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Last name — used when creating a new user' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
lastName?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ type: [AdminOrderItemDto], description: 'Order items' })
|
@ApiProperty({ type: [AdminOrderItemDto], description: 'Order items' })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@@ -67,21 +57,10 @@ export class AdminCreateOrderDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
tableNumber?: string;
|
tableNumber?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({ description: 'Saved user address ID (required for courier delivery)' })
|
||||||
description: 'User delivery address (required for courier delivery)',
|
|
||||||
example: {
|
|
||||||
fullName: 'علی محمدی',
|
|
||||||
phone: '09121234567',
|
|
||||||
city: 'تهران',
|
|
||||||
province: 'تهران',
|
|
||||||
address: 'خیابان ولیعصر، پلاک ۱۲',
|
|
||||||
postalCode: '1234567890',
|
|
||||||
latitude: 35.6892,
|
|
||||||
longitude: 51.389,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
userAddress?: OrderUserAddress;
|
@IsString()
|
||||||
|
addressId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Car address (required for car delivery)',
|
description: 'Car address (required for car delivery)',
|
||||||
@@ -94,22 +73,4 @@ export class AdminCreateOrderDto {
|
|||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
carAddress?: OrderCarAddress;
|
carAddress?: OrderCarAddress;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Payment description. At least one of paymentDesc or paymentAttachments is required for credit card.',
|
|
||||||
example: 'Transfer from account ending 1234',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
paymentDesc?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Payment receipt attachments (URLs). At least one of paymentDesc or paymentAttachments is required for credit card.',
|
|
||||||
type: [String],
|
|
||||||
example: ['https://example.com/receipt.jpg'],
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
paymentAttachments?: string[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { EntityManager } from '@mikro-orm/postgresql';
|
|||||||
import { Order } from '../entities/order.entity';
|
import { Order } from '../entities/order.entity';
|
||||||
import { OrderItem } from '../entities/order-item.entity';
|
import { OrderItem } from '../entities/order-item.entity';
|
||||||
import { User } from '../../users/entities/user.entity';
|
import { User } from '../../users/entities/user.entity';
|
||||||
|
import { UserAddress } from '../../users/entities/user-address.entity';
|
||||||
import { UserRestaurant } from '../../users/entities/user-restuarant.entity';
|
import { UserRestaurant } from '../../users/entities/user-restuarant.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';
|
||||||
@@ -27,7 +28,6 @@ import { OrderMessage } from 'src/common/enums/message.enum';
|
|||||||
import { AdminCreateOrderDto } from '../dto/admin-create-order.dto';
|
import { AdminCreateOrderDto } from '../dto/admin-create-order.dto';
|
||||||
import { FoodOrderReportDto, FoodOrderReportSortBy } from '../dto/food-order-report.dto';
|
import { FoodOrderReportDto, FoodOrderReportSortBy } from '../dto/food-order-report.dto';
|
||||||
import { DailyOrderReportDto, DailyOrderReportSortBy } from '../dto/daily-order-report.dto';
|
import { DailyOrderReportDto, DailyOrderReportSortBy } from '../dto/daily-order-report.dto';
|
||||||
import { normalizePhone } from 'src/modules/utils/phone.util';
|
|
||||||
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
|
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
|
||||||
|
|
||||||
type ValidatedCartForOrder = {
|
type ValidatedCartForOrder = {
|
||||||
@@ -147,18 +147,8 @@ console.log(cart);
|
|||||||
return { paymentUrl, order };
|
return { paymentUrl, order };
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrderForUserByPhone(restaurantId: string, dto: AdminCreateOrderDto): Promise<Order> {
|
async createOrderForUser(restaurantId: string, dto: AdminCreateOrderDto): Promise<Order> {
|
||||||
const normalizedPhone = normalizePhone(dto.userPhone);
|
const user = await this.getUserOrFail(dto.userId);
|
||||||
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([
|
const [restaurant, delivery] = await Promise.all([
|
||||||
this.getRestaurantOrFail(restaurantId),
|
this.getRestaurantOrFail(restaurantId),
|
||||||
@@ -167,10 +157,14 @@ console.log(cart);
|
|||||||
|
|
||||||
const paymentMethod = await this.getPaymentMethodOrFail(dto.paymentMethodId, restaurantId);
|
const paymentMethod = await this.getPaymentMethodOrFail(dto.paymentMethodId, restaurantId);
|
||||||
this.assertPaymentMethodEnabled(paymentMethod);
|
this.assertPaymentMethodEnabled(paymentMethod);
|
||||||
this.assertCreditCardPaymentDetails(paymentMethod, dto.paymentDesc, dto.paymentAttachments);
|
|
||||||
|
|
||||||
this.assertAdminDeliveryRequirements(dto, delivery);
|
this.assertAdminDeliveryRequirements(dto, delivery);
|
||||||
|
|
||||||
|
const userAddress =
|
||||||
|
delivery.method === DeliveryMethodEnum.DeliveryCourier
|
||||||
|
? await this.resolveUserAddressForOrder(user, dto.addressId!)
|
||||||
|
: null;
|
||||||
|
|
||||||
const orderItemsData = await this.buildOrderItemsDataFromDto(dto.items, restaurantId);
|
const orderItemsData = await this.buildOrderItemsDataFromDto(dto.items, restaurantId);
|
||||||
|
|
||||||
const subTotal = orderItemsData.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
|
const subTotal = orderItemsData.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
|
||||||
@@ -185,7 +179,7 @@ console.log(cart);
|
|||||||
user,
|
user,
|
||||||
restaurant,
|
restaurant,
|
||||||
deliveryMethod: delivery,
|
deliveryMethod: delivery,
|
||||||
userAddress: delivery.method === DeliveryMethodEnum.DeliveryCourier ? (dto.userAddress ?? null) : null,
|
userAddress,
|
||||||
carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (dto.carAddress ?? null) : null,
|
carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (dto.carAddress ?? null) : null,
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
couponDiscount: 0,
|
couponDiscount: 0,
|
||||||
@@ -219,7 +213,6 @@ console.log(cart);
|
|||||||
status: PaymentStatusEnum.Pending,
|
status: PaymentStatusEnum.Pending,
|
||||||
method: order.paymentMethod.method,
|
method: order.paymentMethod.method,
|
||||||
gateway: order.paymentMethod.gateway ?? null,
|
gateway: order.paymentMethod.gateway ?? null,
|
||||||
...this.buildCreditCardPaymentFields(paymentMethod, dto.paymentDesc, dto.paymentAttachments),
|
|
||||||
});
|
});
|
||||||
em.persist(payment);
|
em.persist(payment);
|
||||||
|
|
||||||
@@ -229,7 +222,7 @@ console.log(cart);
|
|||||||
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
|
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
|
||||||
await em.flush();
|
await em.flush();
|
||||||
|
|
||||||
this.logger.debug(`Admin created order ${order.id} for user ${user.id} (phone: ${normalizedPhone}, restaurant: ${restaurantId})`);
|
this.logger.debug(`Admin created order ${order.id} for user ${user.id} (restaurant: ${restaurantId})`);
|
||||||
return order;
|
return order;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -467,6 +460,28 @@ console.log(cart);
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveUserAddressForOrder(user: User, addressId: string): Promise<OrderUserAddress> {
|
||||||
|
const address = await this.em.findOne(UserAddress, {
|
||||||
|
id: addressId,
|
||||||
|
user: { id: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
throw new NotFoundException(`Address with ID ${addressId} not found for user ${user.id}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullName: [user.firstName, user.lastName].filter(Boolean).join(' '),
|
||||||
|
phone: address.phone || user.phone,
|
||||||
|
city: address.city,
|
||||||
|
province: address.province || '',
|
||||||
|
address: address.address,
|
||||||
|
postalCode: address.postalCode ?? undefined,
|
||||||
|
latitude: address.latitude,
|
||||||
|
longitude: address.longitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
|
private async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
|
||||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||||
if (!restaurant) throw new NotFoundException(OrderMessage.RESTAURANT_NOT_FOUND);
|
if (!restaurant) throw new NotFoundException(OrderMessage.RESTAURANT_NOT_FOUND);
|
||||||
@@ -619,7 +634,7 @@ console.log(cart);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !dto.userAddress) {
|
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !dto.addressId) {
|
||||||
throw new BadRequestException(OrderMessage.ADDRESS_REQUIRED);
|
throw new BadRequestException(OrderMessage.ADDRESS_REQUIRED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user