This commit is contained in:
2026-02-19 11:20:15 +03:30
parent d32367f37d
commit c05e9187df
18 changed files with 1811 additions and 2 deletions
@@ -1,7 +1,8 @@
import { Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core';
import { Collection, Entity, ManyToOne, OptionalProps, Property,OneToMany } from '@mikro-orm/core';
import { Product } from 'src/modules/product/entities/product.entity';
import { BaseEntity } from 'src/common/entities/base.entity';
import { Invoice } from './invoice.entity';
import { Order } from 'src/modules/order/entities/order.entity';
@Entity({ tableName: 'invoice_items' })
@@ -14,11 +15,14 @@ export class InvoiceItem extends BaseEntity {
@ManyToOne(() => Product)
product!: Product;
@OneToMany(() => Order, order => order.invoiceItem)
orders = new Collection<Order>(this);
@Property({ type: 'int' })
quantity!: number;
@Property({ type: 'int'})
@Property({ type: 'int' })
unitPrice: number;
@Property({
@@ -0,0 +1,216 @@
import { Controller, Get, Post, Param, UseGuards, Patch, Query, Body, Delete } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { OrderService } from '../providers/order.service';
import { AuthGuard } from '../../auth/guards/auth.guard';
import { UserId } from '../../../common/decorators/user-id.decorator';
import { FindOrdersDto } from '../dto/find-orders.dto';
import {
CreateOrderAsUSerDto, CreateOrderItemAsUserDto,
CreateOrderItemDtoAsAdmin, CreateOrderAsAdminDto
} from '../dto/create-order.dto';
import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { AssignDesignerDto } from '../dto/assign-designer.dto';
import { UpdateOrderItemDtoAsAdmin, UpdateOrderItemDtoAsUser } from '../dto/update-order-item.dto';
import { UpdateOrderAsAdminDto } from '../dto/update-order.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { CreatePrintFormDto } from '../dto/create-print-form.dto';
import { FindOrderItemsDto } from '../dto/find-order-items.dto';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { PermissionEnum } from 'src/common/enums/permission.enum';
@ApiTags('orders')
@ApiBearerAuth()
@Controller()
export class OrderController {
constructor(private readonly orderService: OrderService) { }
// ================== Public ===================
@Post('public/orders')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'create order ' })
createOrder(@UserId() userId: string, @Body() body: CreateOrderAsUSerDto) {
return this.orderService.createOrderAsUser(userId, body);
}
@Delete('public/orders/:id')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Delete order ' })
deleteOrder(@Param('id') orderId: string, @UserId() userId: string) {
return this.orderService.removeOrderAsUser(userId, orderId);
}
@Get('public/orders')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Get all orders with pagination and filters' })
findAll(@Query() dto: FindOrdersDto, @UserId() userId: string) {
return this.orderService.findOrdersAsUser(userId, dto);
}
@Get('public/orders/:id')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'order detail' })
orderDetail(@Param('id') orderId: string, @UserId() userId: string) {
return this.orderService.getOrderAsUser(userId, orderId);
}
@Post('public/orders/:id/item')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'add item to order ' })
addOrderItem(@Param('id') orderId: string, @UserId() userId: string, @Body() body: CreateOrderItemAsUserDto) {
return this.orderService.addOrderItemAsUser(userId, orderId, body);
}
@Patch('public/orders/:id/item/:itemId')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Update order item ' })
updateOrderItem(@Param('id') orderId: string, @UserId() userId: string, @Param('itemId') itemId: string, @Body() body: UpdateOrderItemDtoAsUser) {
return this.orderService.updateOrderItemAsUser(userId, orderId, itemId, body);
}
@Patch('public/orders/:orderId/items/:orderItemId/confirm')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Confirm Invoice Item ' })
confirmOrderItem(
@Param('orderId') orderId: string,
@Param('orderItemId') orderItemId: string,
@UserId() userId: string
) {
return this.orderService.confirmOrderItem(userId, orderId, orderItemId);
}
@Delete('public/orders/:orderId/items/:orderItemId')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Remove order Item ' })
removeOrderItem(
@Param('orderId') orderId: string,
@Param('orderItemId') orderItemId: string,
@UserId() userId: string
) {
return this.orderService.removeOrderItemAsUser(userId, orderId, orderItemId);
}
/*========================== Admin Routes =====================*/
@Post('admin/orders')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.CREATE_ORDER)
@ApiOperation({ summary: 'create order ' })
createOrderAsAdmin(@AdminId() adminId: string, @Body() body: CreateOrderAsAdminDto) {
return this.orderService.createOrderAsAdmin(adminId, body);
}
@Get('admin/orders/request')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.VIEW_REQUESTS)
@ApiOperation({ summary: 'Get all order requests with pagination and filters' })
findOrderRequestAsAdmin(@Query() dto: FindOrdersDto) {
return this.orderService.findOrderRequestsAsAdmin(dto);
}
// this route permission is handled inside service
@Get('admin/orders/invoiced')
@UseGuards(AdminAuthGuard)
@ApiOperation({ summary: 'Get all invoiced orders with pagination and filters' })
findInvoicedOrdersAsAdmin(@Query() dto: FindOrdersDto, @AdminId() adminId: string) {
return this.orderService.findInvoicedOrdersAsAdmin(adminId, dto);
}
@Get('admin/orders/:id')
@UseGuards(AdminAuthGuard)
@ApiOperation({ summary: 'order detail' })
orderDetailAsAdmin(@Param('id') orderId: string) {
return this.orderService.findOrderOrFail(orderId);
}
@Patch('admin/orders/:id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.UPDATE_ORDER)
@ApiOperation({ summary: 'Update Order ' })
updateOrder(@Param('id') id: string, @Body() dto: UpdateOrderAsAdminDto) {
return this.orderService.updateOrderAsAdmin2(id, dto);
}
@Delete('admin/orders/:orderId')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.REMOVE_ORDER)
@ApiOperation({ summary: 'Hard Delete Order ' })
hardDelete(@Param('orderId') orderId: string) {
return this.orderService.hardDeleteOrder(orderId);
}
// =============== Order Item ================
@Get('admin/orders/item/print')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.VIEW_All_ORDERS)
@ApiOperation({ summary: 'get print orders' })
getPrintOrders(@Body() dto: FindOrderItemsDto) {
return this.orderService.findPrintOrderItemsAsAdmin(dto);
}
@Get('admin/orders/item/:id')
@UseGuards(AdminAuthGuard)
@ApiOperation({ summary: 'order item detail' })
getOrderItem(@Param('id') itemId: string) {
return this.orderService.findOrderItemOrFailByItemId(itemId);
}
@Post('admin/orders/:orderId/item/:itemId/designer')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.ASSIGN_DESIGNER)
@ApiOperation({ summary: 'Assign Order Designer ' })
assignDesigner(@Param('orderId') orderId: string, @Param('itemId') itemId: string,
@Body() body: AssignDesignerDto) {
return this.orderService.assignDesigner(orderId, itemId, body.designerId);
}
@Patch('admin/orders/:id/item/:itemId/print')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.CREATE_ORDER_PRINT)
@ApiOperation({ summary: 'Create Print form for order item' })
createPrintForm(@Param('id') id: string, @Param('itemId') itemId: string, @Body() dto: CreatePrintFormDto) {
return this.orderService.createPrintForm(id, itemId, dto);
}
// @Post('admin/orders/:orderId/invoice')
// @UseGuards(AdminAuthGuard)
// @ApiOperation({ summary: 'Create Order invoice ' })
// createInvoice(@Param('orderId') orderId: string) {
// return this.orderService.createInvoice(orderId);
// }
@Delete('admin/orders/:id/items/:itemId')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.UPDATE_ORDER)
@ApiOperation({ summary: 'Remove order Item ' })
removeOrderItemAdAdmin(
@Param('id') orderId: string,
@Param('itemId') itemId: string,
) {
return this.orderService.removeOrderItemAsAdmin(orderId, itemId);
}
@Post('admin/orders/:id/item')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.UPDATE_ORDER)
@ApiOperation({ summary: 'add item to order ' })
addOrderItemAsAdmin(@Param('id') orderId: string, @Body() body: CreateOrderItemDtoAsAdmin) {
return this.orderService.addOrderItemAsAdmin(orderId, body);
}
@Patch('admin/orders/:id/item/:itemId')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.UPDATE_ORDER)
@ApiOperation({ summary: 'Update order item' })
updateOrderItemAsAdmin(@Param('id') orderId: string, @Param('itemId') itemId: string,
@Body() body: UpdateOrderItemDtoAsAdmin) {
return this.orderService.updateOrderItemAsAdmin(orderId, itemId, body);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { EntityManager } from '@mikro-orm/postgresql';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class OrdersCrone {
private readonly logger = new Logger(OrdersCrone.name);
constructor(
private readonly em: EntityManager,
private readonly eventEmitter: EventEmitter2,
) { }
}
@@ -0,0 +1,11 @@
import {
IsString
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AssignDesignerDto {
@IsString()
@ApiProperty()
designerId: string;
}
+146
View File
@@ -0,0 +1,146 @@
import {
IsString,
IsInt, IsArray, IsNumber,
IsNotEmpty,
ArrayMinSize,
IsOptional,
IsBoolean,
IsEnum,
IsDateString
} from 'class-validator';
import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IAttachment, IField, OrderItemStatusEnum } from '../interface/order.interface';
export class CreateOrderItemAsUserDto {
@IsString()
@ApiProperty()
productId: string;
@ApiProperty()
@IsNumber()
quantity: number
@ApiProperty({ enum: OrderItemStatusEnum })
@IsEnum(() => OrderItemStatusEnum)
status: OrderItemStatusEnum
@ApiProperty()
@IsArray()
attributes: IField[]
@ApiPropertyOptional({ example: 'توضیحات' })
@IsString()
description: string
@ApiPropertyOptional({ example: [] })
@IsArray()
attachments: { url: string, type: string }[]
}
export class CreateOrderItemDtoAsAdmin extends CreateOrderItemAsUserDto {
// @ApiPropertyOptional({ example: [] })
// @IsArray()
// printAttributes?: IField[]
// @ApiPropertyOptional({ example: [] })
// @IsArray()
// printAttachments?: IAttachment[]
@IsInt()
@ApiProperty()
designerId: string;
@ApiProperty()
@IsNumber()
unitPrice: number
@ApiProperty()
@IsNumber()
discount: number
@ApiPropertyOptional()
@IsString()
adminDescription: string
@ApiPropertyOptional()
@IsDateString()
confirmedAt: string
}
export class CreateOrderAsUSerDto {
@IsArray()
@ApiProperty({
isArray: true, type: [CreateOrderItemAsUserDto],
example: [
{
productId: 'sd4',
quantity: 100,
attributes: [
{
attributeId: 1,
value: 'string | boolean| number'
}
],
attachments: [
{ url: '', type: '' }
],
description: ''
}
]
})
@IsNotEmpty()
@ArrayMinSize(1, { message: 'At least one item is required' })
@Type(() => CreateOrderItemAsUserDto)
items: CreateOrderItemAsUserDto[];
}
export class CreateOrderAsAdminDto {
@ApiProperty({ example: '' })
@IsString()
@IsNotEmpty()
userId: string
@IsArray()
@ApiProperty({
isArray: true, type: [CreateOrderItemDtoAsAdmin], example: [
{
productId: 1,
designerId: '',
attachments: [{ url: '', type: '' }],
quantity: 100,
attributes: [{ fieldId: 10, value: 'blue' }],
unitPrice: 150000,
discount: 20000,
adminDescription: '',
description: 'توضیحات کاربر',
}
]
})
@IsNotEmpty()
@ArrayMinSize(1, { message: 'At least one product is required' })
@Type(() => CreateOrderItemDtoAsAdmin)
items: CreateOrderItemDtoAsAdmin[];
@ApiProperty({})
@IsString()
@IsOptional()
paymentMethod: string
@ApiProperty({})
@IsBoolean()
enableTax: boolean
@ApiProperty({})
@IsInt()
estimatedDays: number
}
@@ -0,0 +1,16 @@
import { IsArray, } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IField } from '../interface/order.interface';
export class CreatePrintFormDto {
@ApiPropertyOptional({
example: [{
fieldId: 10, value: ''
}]
})
@IsArray()
printAttributes?: IField[]
}
@@ -0,0 +1,53 @@
import {
IsOptional,
IsString,
IsNumber,
Min,
IsIn,
IsEnum,
IsDateString,
IsArray,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { OrderStatusEnum } from '../interface/order.interface';
import { PaymentStatusEnum } from '../../payment/interface/payment';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindOrderItemsDto {
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page: number = 1;
@ApiPropertyOptional({ default: 10, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit: number = 10;
@ApiPropertyOptional()
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ default: 'createdAt' })
@IsOptional()
@IsString()
orderBy: string = 'createdAt';
@ApiPropertyOptional({
enum: sortOrderOptions,
default: 'desc',
})
@IsOptional()
@IsIn(sortOrderOptions)
order: SortOrder = 'desc';
}
+80
View File
@@ -0,0 +1,80 @@
import {
IsOptional,
IsString,
IsNumber,
Min,
IsIn,
IsEnum,
IsDateString,
IsArray,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { OrderStatusEnum } from '../interface/order.interface';
import { PaymentStatusEnum } from '../../payment/interface/payment';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindOrdersDto {
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page: number = 1;
@ApiPropertyOptional({ default: 10, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit: number = 10;
@ApiPropertyOptional({
description: 'Filter by order statuses',
enum: OrderStatusEnum,
isArray: true,
})
@IsOptional()
@Transform(({ value }) =>
Array.isArray(value) ? value : value?.split(',')
)
@IsArray()
@IsEnum(OrderStatusEnum, { each: true })
statuses?: OrderStatusEnum[];
// @ApiPropertyOptional({ enum: PaymentStatusEnum })
// @IsOptional()
// @IsEnum(PaymentStatusEnum)
// paymentStatus?: PaymentStatusEnum;
@ApiPropertyOptional()
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
from?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
to?: string;
@ApiPropertyOptional({ default: 'createdAt' })
@IsOptional()
@IsString()
orderBy: string = 'createdAt';
@ApiPropertyOptional({
enum: sortOrderOptions,
default: 'desc',
})
@IsOptional()
@IsIn(sortOrderOptions)
order: SortOrder = 'desc';
}
@@ -0,0 +1,7 @@
import { PartialType } from '@nestjs/swagger';
import { CreateOrderItemAsUserDto, CreateOrderItemDtoAsAdmin } from './create-order.dto';
export class UpdateOrderItemDtoAsUser extends PartialType(CreateOrderItemAsUserDto) { }
export class UpdateOrderItemDtoAsAdmin extends PartialType(CreateOrderItemDtoAsAdmin) { }
+14
View File
@@ -0,0 +1,14 @@
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
import { CreateOrderAsAdminDto, CreateOrderItemDtoAsAdmin } from './create-order.dto';
import { IsNumber } from 'class-validator';
export class UpdateOrderAsAdminDto extends PartialType(OmitType(CreateOrderAsAdminDto, 'items' as any)) {
@ApiProperty()
items: UpdateOrCreateOrderItem[]
}
export class UpdateOrCreateOrderItem extends CreateOrderItemDtoAsAdmin {
@ApiProperty()
@IsNumber()
id: string
}
@@ -0,0 +1,55 @@
import {
Entity,
Index,
ManyToOne,
Property,
Enum,
PrimaryKey,
OptionalProps,
} from '@mikro-orm/core';
import { User } from '../../user/entities/user.entity';
import { ulid } from 'ulid';
import { Admin } from 'src/modules/admin/entities/admin.entity';
import { BaseEntity } from 'src/common/entities/base.entity';
import { OrderStatusEnum } from '../interface/order.interface';
import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity';
import { IField } from 'src/modules/request/interface/request.interface';
import { Product } from 'src/modules/product/entities/product.entity';
@Entity({ tableName: 'orders' })
@Index({ properties: ['user'] })
export class Order extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt' | 'orderNumber'
@ManyToOne(() => InvoiceItem)
invoiceItem: InvoiceItem
@ManyToOne(() => Product)
product: Product;
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid()
@ManyToOne(() => User)
user!: User;
@ManyToOne(() => Admin, { nullable: true })
designer?: Admin
@ManyToOne(() => Admin, { nullable: true })
creator?: Admin
@Property({
type: 'int',
autoincrement: true
})
orderNumber: number;
@Property({ type: 'json', nullable: true })
attributes?: IField[] // print form selected attributes
@Enum(() => OrderStatusEnum)
status: OrderStatusEnum = OrderStatusEnum.CREATED;
}
+21
View File
@@ -0,0 +1,21 @@
import type { OrderStatusEnum } from '../interface/order.interface';
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly: string,
public readonly orderNumber: string,
public readonly total: number,
) { }
}
export class OrderStatusChangedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly orderNumber: string,
public readonly: string,
public readonly previousStatus: OrderStatusEnum,
public readonly newStatus: OrderStatusEnum,
) { }
}
@@ -0,0 +1,47 @@
export enum OrderStatusEnum {
CREATED = 'created',
INVOICED = 'invoiced',
WAITING_to_CONFIRM_INVOICE = 'waiting_to_confirm_invoice',
DESIGN_INITIATED = 'design_initiated',
DESIGN_FINISHED = 'design_finished',
DESIGN_REVISION = 'design_revision',
READY_TO_PRINTING = 'ready_to_printing',
IN_PRINTING = 'in_printing',
READY_FOR_DELIVERY = 'ready_for_delivery',
DELIVERED = 'delivered',
AFTER_PRINT_COVER = 'after_print_cover',
AFTER_PRINT_BOXING = 'after_print_boxing',
AFTER_PRINT_CARTONING = 'after_print_cartoning',
FINISHED = 'finished',
CANCELED = 'canceled',
}
export interface IAttachment { type: string, url: string }
export interface CreateOrder {
userId: string
// status: OrderStatusEnum
enableTax?: boolean
paymentMethod?: string
adminId?: string
estimatedDays?: number
}
export type UpdateOrder = Partial<CreateOrder>
@@ -0,0 +1,212 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
import { PermissionEnum } from 'src/common/enums/permission.enum';
import { NotifEvent } from 'src/modules/notification/interfaces/notification.interface';
import { NotificationService } from 'src/modules/notification/services/notification.service';
import { ConfigService } from '@nestjs/config';
import { OrderRepository } from '../repositories/order.repository';
// import { OrderStatusEnum } from '../interface/order.interface';
import { PaymentMethodEnum } from 'src/modules/payment/interface/payment';
import { UserService } from 'src/modules/user/providers/user.service';
@Injectable()
export class OrderListeners {
private readonly logger = new Logger(OrderListeners.name);
private orderCreatedSmsTemplateId: string;
private orderStatusChangedSmsTemplateId: string;
// private orderCompletedSmsTemplateId: string;
constructor(
private readonly adminService: AdminRepository,
private readonly OrderRepository: OrderRepository,
private readonly notificationService: NotificationService,
private readonly configService: ConfigService,
private readonly userService: UserService,
) {
this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123';
this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123';
// this.orderCompletedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123';
}
// private getStatusFarsi(status: OrderStatus): string {
// const statusMap: Record<OrderStatus, string> = {
// [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت',
// [OrderStatus.PAID]: 'پرداخت شده',
// [OrderStatus.PREPARING]: 'در حال آماده‌سازی',
// [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش',
// [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون',
// [OrderStatus.SHIPPED]: 'ارسال شده',
// [OrderStatus.COMPLETED]: 'تکمیل شده',
// [OrderStatus.CANCELED]: 'لغو شده',
// };
// return statusMap[status] || status;
// }
// @OnEvent(OrderCreatedEvent.name)
// async handleOrderCreated(event: OrderCreatedEvent) {
// try {
// this.logger.log(
// `Order created event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
// const order = await this.OrderRepository.findOne(event.orderId);
// if (order?.paymentMethod.method === PaymentMethodEnum.Online) {
// return;
// }
// // get admnin os restuaraant that have order permissuins
// const admins = await this.adminService.findAdminsWithPermission(event., Permission.MANAGE_ORDERS);
// const recipients = admins.map(admin => ({
// adminId: admin.id,
// }));
// await this.notificationService.sendNotification({
// message: {
// title: NotifTitleEnum.ORDER_CREATED,
// content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
// sms: {
// templateId: this.orderCreatedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// total: event.total.toString(),
// },
// },
// pushNotif: {
// title: `سفارش جدید`,
// content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_CREATED,
// url: `/`,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// } catch (error) {
// this.logger.error(
// `Failed to send notification for order created event: ${event.}`,
// error instanceof Error ? error.stack : String(error),
// );
// }
// }
// @OnEvent(OrderStatusChangedEvent.name)
// async handleOrderStatusChanged(event: OrderStatusChangedEvent) {
// try {
// this.logger.log(
// `Order status changed event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
// //TODO : REFACTOR to use queue or other way to handle this
// const recipients = [
// {
// userId: event.userId,
// },
// ];
// if (event.newStatus === OrderStatus.COMPLETED) {
// if (!event?.userId) {
// this.logger.log(
// `User not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
// }
// // const restaurant = await this.RestaurantRepository.findOne(event.);
// // if (!restaurant) {
// // this.logger.log(
// // `Restaurant not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // const score = restaurant.score;
// // if (!score) {
// // this.logger.log(
// // `Score not found for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // increase score for user
// // const order = await this.OrderRepository.findOne(event.orderId);
// // if (!order) {
// // this.logger.log(
// // `Order not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // this.userService.createWalletTransaction(event.userId, event., {
// // amount: order.subTotal,
// // type: WalletTransactionType.CREDIT,
// // reason: WalletTransactionReason.ORDER_COMPLETED_DEPOSIT,
// // });
// await this.notificationService.sendNotification({
// : event.,
// message: {
// title: NotifTitleEnum.ORDER_STATUS_CHANGED,
// content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`,
// sms: {
// templateId: this.orderCreatedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// },
// },
// pushNotif: {
// title: `تغییر وضعیت سفارش`,
// content: `لطفا برای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_STATUS_CHANGED,
// url: ``,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// } else {
// await this.notificationService.sendNotification({
// : event.,
// message: {
// title: NotifTitleEnum.ORDER_STATUS_CHANGED,
// content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
// sms: {
// templateId: this.orderStatusChangedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// status: this.getStatusFarsi(event.newStatus),
// },
// },
// pushNotif: {
// title: `تغییر وضعیت سفارش`,
// content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_STATUS_CHANGED,
// url: ``,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// }
// } catch (error) {
// this.logger.error(
// `Failed to send notification for order status changed event: ${event.}`,
// error instanceof Error ? error.stack : String(error),
// );
// }
// }
}
+39
View File
@@ -0,0 +1,39 @@
import { Module, forwardRef } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { OrderService } from './providers/order.service';
import { OrderController } from './controllers/order.controller';
import { Order } from './entities/order.entity';
import { OrderItem } from './entities/order-item.entity';
import { AuthModule } from '../auth/auth.module';
import { PaymentModule } from '../payment/payments.module';
import { JwtModule } from '@nestjs/jwt';
import { OrderRepository } from './repositories/order.repository';
import { OrderListeners } from './listeners/order.listeners';
import { OrdersCrone } from './crone/order.crone';
import { AdminModule } from '../admin/admin.module';
import { NotificationsModule } from '../notification/notifications.module';
import { UserModule } from '../user/user.module';
import { UtilsModule } from '../util/utils.module';
import { ProductModule } from '../product/product.module';
import { TicketModule } from '../ticket/ticket.module';
import { OrderItemRepository } from './repositories/order-item.repository';
@Module({
imports: [
MikroOrmModule.forFeature([Order, OrderItem]),
AuthModule,
forwardRef(() => PaymentModule),
JwtModule,
AdminModule,
NotificationsModule,
UtilsModule,
forwardRef(() => UserModule),
ProductModule,
forwardRef(() => TicketModule)
],
controllers: [OrderController],
providers: [OrderService, OrderRepository, OrderListeners,
OrdersCrone, OrderItemRepository],
exports: [OrderRepository, OrderService, OrderItemRepository],
})
export class OrderModule { }
@@ -0,0 +1,636 @@
import { Injectable, BadRequestException, Logger, BadGatewayException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { OrderRepository } from '../repositories/order.repository';
import { FindOrdersDto } from '../dto/find-orders.dto';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
CreateOrderAsAdminDto,
CreateOrderAsUSerDto, CreateOrderItemAsUserDto,
CreateOrderItemDtoAsAdmin
} from '../dto/create-order.dto';
import { UserService } from 'src/modules/user/providers/user.service';
import {
OrderItemStatusEnum,
CreateOrderWithItems,
CreateOrderItemPopulated,
UpdateOrder,
UpdateOrderItem,
OrderStatusEnum
} from '../interface/order.interface';
import { OrderItemRepository } from '../repositories/order-item.repository';
import { ProductService } from 'src/modules/product/providers/product.service';
import { ProductRepository } from 'src/modules/product/repositories/product.repository';
import { TicketService } from 'src/modules/ticket/providers/tickets.service';
import { TicketRepository } from 'src/modules/ticket/repositories/tickets.repository';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { Order } from '../entities/order.entity';
import { PaymentRepository } from 'src/modules/payment/repositories/payment.repository';
import { UpdateOrderItemDtoAsAdmin, UpdateOrderItemDtoAsUser } from '../dto/update-order-item.dto';
import { Admin } from 'src/modules/admin/entities/admin.entity';
import { AdminService } from 'src/modules/admin/providers/admin.service';
import { OrderItem } from '../entities/order-item.entity';
import { UpdateOrderAsAdminDto } from '../dto/update-order.dto';
import { CreatePrintFormDto } from '../dto/create-print-form.dto';
import { FindOrderItemsDto } from '../dto/find-order-items.dto';
import { PermissionService } from 'src/modules/roles/providers/permissions.service';
import { PermissionEnum } from 'src/common/enums/permission.enum';
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
constructor(
private readonly em: EntityManager,
private readonly orderRepository: OrderRepository,
private readonly orderItemRepository: OrderItemRepository,
private readonly userService: UserService,
private readonly adminService: AdminService,
private readonly productService: ProductService,
private readonly productRepository: ProductRepository,
private readonly ticketRepository: TicketRepository,
private readonly ticketService: TicketService,
private readonly eventEmitter: EventEmitter2,
private readonly adminRepository: AdminRepository,
private readonly paymentRepository: PaymentRepository,
private readonly permissionService: PermissionService,
) { }
async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto) {
const order = await this.createOrder({ ...dto, adminId })
// await this.calculateOrder(order)
// this.em.flush()
this.logger.log("Order created as admin")
return order
}
async createOrderAsUser(userId: string, dto: CreateOrderAsUSerDto) {
const order = await this.createOrder({ ...dto, userId })
this.logger.log("Order created as user")
return order
}
async updateOrderAsAdmin(orderId: string, dto: UpdateOrderAsAdminDto) {
const order = await this.findOrderOrFail(orderId)
// const updateOrder = await this.updateOrder(order, dto)
// const updateOrder = await this.calculateOrder(order)
// await this.em.flush()
this.logger.log("Order updated as admin")
// return updateOrder
}
async updateOrderAsAdmin2(orderId: string, dto: UpdateOrderAsAdminDto) {
const order = await this.findOrderOrFail(orderId)
const { items, ...rest } = dto
return this.em.transactional(async (em) => {
em.assign(order, rest)
for (const item of items) {
if (item.id) { // update
const currentItem = order.items.find(i => i.id == item.id)
if (!currentItem) {
throw new BadGatewayException("Order Item not found")
}
const { designerId, productId, ...itemRest } = item
let newProduct = currentItem.product
if (productId && productId !== currentItem.product.id) {
newProduct = await this.productService.findOneOrFail(productId)
}
let newDesigner: Admin | undefined
if (designerId && designerId !== currentItem.designer?.id) {
newDesigner = await this.adminService.findOrFail(designerId)
}
em.assign(currentItem, { ...itemRest, designer: newDesigner, product: newProduct })
} else {
if (!item.productId) {
throw new BadRequestException(`Product id is required for item ${item.id}`)
}
const product = await this.productService.findOneOrFail(item.productId)
const newOrderItem = em.create(OrderItem, {
order,
product,
quantity: item.quantity,
unitPrice: item.unitPrice,
adminDescription: item.adminDescription,
attachments: item.attachments,
attributes: item.attributes,
description: item.description,
discount: item.discount,
confirmedAt: item.confirmedAt,
status: OrderItemStatusEnum.CREATED
})
em.persist(newOrderItem)
}
}
await em.flush()
this.logger.log(`order #${order.orderNumber} was updated as Admin`)
return order
})
}
async addOrderItemAsUser(userId: string, orderId: string, dto: CreateOrderItemAsUserDto) {
const order = await this.findOrderOrFail(orderId)
// if (order.status !== OrderStatusEnum.CREATED) {
// throw new BadRequestException(`You can not update when status is ${order.status}`)
// }
if (order.user.id !== userId) {
throw new BadRequestException(`This order doest belongs to you!`)
}
const product = await this.productService.findOneOrFail(dto.productId)
const orderItem = this.createOrderItemEntity(order, { ...dto, product })
await this.em.flush()
return orderItem
}
async addOrderItemAsAdmin(orderId: string, dto: CreateOrderItemDtoAsAdmin) {
const order = await this.findOrderOrFail(orderId)
const product = await this.productService.findOneOrFail(dto.productId)
let designer: Admin | undefined
if (dto.designerId) {
designer = await this.adminService.findOrFail(dto.designerId)
}
const orderItem = this.createOrderItemEntity(order, { ...dto, designer, product })
await this.em.flush()
// await this.calculateOrder(undefined, orderId)
// await this.em.flush()
return orderItem
}
async updateOrderItemAsUser(userId: string, orderId: string, itemId: string, dto: UpdateOrderItemDtoAsUser) {
const orderItem = await this.findOrderItemOrFail(orderId, itemId)
if (orderItem.order.user.id !== userId) {
throw new BadRequestException(`This order doesnt belongs to you`)
}
// if (orderItem.order.status !== OrderStatusEnum.CREATED) {
// throw new BadRequestException(`You can not update when status is ${orderItem.order.status}`)
// }
if (orderItem.confirmedAt) {
throw new BadRequestException(`You can not update when item is confirmed`)
}
const updated = await this.updateOrderItem(orderItem, dto)
return updated
}
async updateOrderItemAsAdmin(orderId: string, itemId: string, dto: UpdateOrderItemDtoAsAdmin) {
const orderItem = await this.findOrderItemOrFail(orderId, itemId)
const updated = await this.updateOrderItem(orderItem, dto)
// await this.calculateOrder(undefined, orderId)
// await this.em.flush()
return updated
}
async createPrintForm(orderId: string, itemId: string, dto: CreatePrintFormDto) {
const orderItem = await this.findOrderItemOrFail(orderId, itemId)
const updated = await this.updateOrderItem(orderItem, { ...dto, printFormCreatedAt: new Date() })
return updated
}
async removeOrderItemAsAdmin(orderId: string, itemId: string) {
const orderItem = await this.findOrderItemOrFail(orderId, itemId)
await this.removeOrderItem(orderItem)
// await this.calculateOrder(undefined, orderId)
// await this.em.flush()
return { message: 'success' }
}
async removeOrderItemAsUser(userId: string, orderId: string, itemId: string) {
const orderItem = await this.findOrderItemOrFail(orderId, itemId)
if (orderItem.order.user.id !== userId) {
throw new BadRequestException(`this order is not belongs to you`)
}
// if (orderItem.order.status !== OrderStatusEnum.CREATED) {
// throw new BadRequestException(`You can not delete when order status is ${orderItem.order.status}`)
// }
if (orderItem.confirmedAt) {
throw new BadRequestException(`You can not delete when item is confirmed`)
}
await this.removeOrderItem(orderItem)
return { message: 'success' }
}
async findOrdersAsUser(userId: string, dto: FindOrdersDto) {
const orders = await this.orderRepository.findAllPaginated({ userId, ...dto })
return orders
}
async findOrderRequestsAsAdmin(dto: FindOrdersDto) {
const orders = await this.orderRepository.findAllPaginated({ ...dto, isInvoiced: false })
return orders
}
async findInvoicedOrdersAsAdmin(adminId: string, dto: FindOrdersDto) {
const canSeeAll = await this.adminService.hasPermission(adminId, PermissionEnum.VIEW_All_INVOICED_ORDERS)
let canSeeAsigned = false
if (!canSeeAll) {
canSeeAsigned = await this.adminService.hasPermission(adminId, PermissionEnum.VIEW_MY_ASSIGNED_INVOICED_REQUESTS)
}
if (!canSeeAsigned && !canSeeAll) {
throw new BadRequestException("You dont have Permission")
}
let designerId = canSeeAll ? undefined : adminId
const orders = await this.orderRepository.findAllPaginated({ ...dto, isInvoiced: true, designerId })
return orders
}
async findPrintOrderItemsAsAdmin(dto: FindOrderItemsDto) {
const orderItems = await this.orderItemRepository.findAllPaginated({ ...dto, isPrintFormAdded: true, })
return orderItems
}
async findPrints(dto: FindOrdersDto) {
const orders = await this.orderRepository.findAllPaginated({ ...dto, statuses: [] })
return orders
}
async confirmOrderItem(userId: string, orderId: string, orderItemId: string) {
const orderItem = await this.findOrderItemOrFail(orderId, orderItemId)
if (orderItem.order.user.id !== userId) {
throw new BadRequestException("Order Item does not belong to you")
}
orderItem.confirmedAt = new Date()
await this.em.persistAndFlush(orderItem)
return orderItem
}
async assignDesigner(orderId: string, orderItemId: string, designerId: string) {
const orderItem = await this.findOrderItemOrFail(orderId, orderItemId)
const designer = await this.adminRepository.findOne({ id: designerId })
if (!designer) {
throw new BadRequestException("designer not found")
}
orderItem.designer = designer
await this.em.persistAndFlush(orderItem)
return orderItem
}
async removeOrderAsUser(userId: string, orderId: string) {
const order = await this.findOrderOrFail(orderId)
if (order.user.id !== userId) {
throw new BadRequestException("this order doesnt belongs to you")
}
// if (![OrderStatusEnum.CREATED].includes(order.status)) {
// throw new BadRequestException("Order can not be deleted")
// }
return this.hardDeleteOrder(orderId)
}
// async updateStatus(orderId: string, newStatus: OrderStatusEnum) {
// const order = await this.findOrderOrFail(orderId)
// // order.status = newStatus
// await this.em.flush()
// return order
// }
// async createInvoice(orderId: string) {
// const order = await this.updateStatus(orderId, OrderStatusEnum.INVOICED)
// return order
// }
async getOrderAsUser(userId: string, orderId: string) {
const order = await this.findOrderOrFail(orderId)
if (order.user.id !== userId) {
throw new BadRequestException("This order is not belongs to you")
}
return order
}
// ==================== Entity Methods ====================
private async createOrderItemEntity(order: Order, dto: CreateOrderItemPopulated, em?: EntityManager) {
const { attributes, description, quantity, attachments, discount,
unitPrice, product, designer, printAttributes, status } = dto
const eM = em ?? this.em
return eM.create(
OrderItem,
{
designer,
order,
attributes,
description,
quantity,
attachments,
discount,
unitPrice,
product,
printAttributes: printAttributes,
status
})
}
// ==================== Core Logic ====================
async createOrder(dto: CreateOrderWithItems) {
const { items, userId, enableTax,
estimatedDays, paymentMethod, adminId } = dto
// Validate and fetch all required entities
const user = await this.userService.findOrFail(userId)
const admin = adminId ? await this.adminService.findOrFail(adminId) : undefined
const products = await this.productService.findProductsByIds(items.map(item => item.productId))
const desiners = await this.adminService.findByIds(items.map(item => item.designerId).filter(id => id != null))
const productMap = new Map(
products.map(p => [Number(p.id), p])
);
const designerMap = new Map(
desiners.map(d => [d.id, d])
)
return this.em.transactional(async (em) => {
const order = em.create(Order, {
creator: admin,
user,
enableTax,
estimatedDays,
paymentMethod,
status: OrderStatusEnum.REQUEST,
})
em.persist(order)
for (const item of items) {
const product = productMap.get(Number(item.productId))
if (!product) {
throw new BadRequestException("Product not found!")
}
let designer: undefined | Admin = undefined
if (item.designerId) {
designer = designerMap.get(item.designerId)
if (!designer) {
throw new BadRequestException("designer not found!")
}
}
const orderItem = await this.createOrderItemEntity(order, { ...item, designer, product }, em)
order.items.add(orderItem)
}
// TODO : calculation must be done after create order
// await this.calculateOrder(order)
await em.flush()
return order
})
}
async updateOrder(order: Order, param: UpdateOrder) {
const { adminId, enableTax,
estimatedDays, paymentMethod, userId } = param
if (userId) {
const user = await this.userService.findOrFail(userId)
order.user = user
}
if (adminId != undefined && adminId !== null) {
const admin = await this.adminService.findOrFail(adminId)
order.creator = admin
}
if (enableTax != undefined) {
order.enableTax = enableTax
}
if (estimatedDays != undefined) {
order.estimatedDays = estimatedDays
}
if (paymentMethod != undefined) {
order.paymentMethod = paymentMethod
}
// if (status != undefined) {
// order.status = status
// if (status === OrderStatusEnum.INVOICED && !order.invoicedAt) {
// order.invoicedAt = new Date()
// }
// }
await this.em.persistAndFlush(order)
return order
}
async updateOrderItem(orderItem: OrderItem, dto: UpdateOrderItem) {
const { attributes, description, product, quantity, printFormCreatedAt,
attachments, discount, unitPrice, designer, printAttributes, } = dto
if (product && orderItem.product.id !== product.id) {
orderItem.product = product
}
if (designer && orderItem.designer?.id !== designer.id) {
orderItem.designer = designer
}
if (attributes != undefined) {
orderItem.attributes = attributes
}
if (description != undefined) {
orderItem.description = description
}
if (quantity != undefined) {
orderItem.quantity = quantity
}
if (attachments != undefined) {
orderItem.attachments = attachments
}
if (discount != undefined) {
orderItem.discount = discount
}
if (unitPrice != undefined) {
orderItem.unitPrice = unitPrice
}
if (printFormCreatedAt != undefined) {
orderItem.printFormCreatedAt = printFormCreatedAt
}
if (printAttributes != undefined) {
orderItem.printAttributes = printAttributes
}
await this.em.flush()
return orderItem
}
async removeOrderItem(orderItem: OrderItem) {
await this.em.removeAndFlush(orderItem)
return true
}
async hardDeleteOrder(orderId: string) {
const order = await this.findOrderOrFail(orderId)
if (order.payments.length > 0) {
throw new BadRequestException("order with payments can not be deleted!")
}
await this.em.transactional(async (em) => {
await this.ticketRepository.nativeDelete({ orderItem: { order: { id: orderId } } })
await this.orderRepository.nativeDelete(order)
// order item will be deleted because cascade is true
// TODO : images also must be deleted
await em.flush()
})
return { message: "Order deleted successfully" }
}
// ==================== Helper Methods ====================
async findOrderOrFail(orderId: string) {
const order = await this.orderRepository.findOne({ id: orderId },
{ populate: ['items', 'items.product', 'payments', 'user', 'items.designer'] })
if (!order) {
throw new BadRequestException('Order not found')
}
return order
}
async findOrderItemOrFail(orderId: string, itemId: string) {
const orderItem = await this.orderItemRepository.findOne({ id: itemId, order: { id: orderId } },
{ populate: ['order', 'order.user'] })
if (!orderItem) {
throw new BadRequestException('Order item not found')
}
return orderItem
}
async findOrderItemOrFailByItemId(itemId: string) {
const orderItem = await this.orderItemRepository.findOne({ id: itemId },
{ populate: ['order', 'order.user'] })
if (!orderItem) {
throw new BadRequestException('Order item not found')
}
return orderItem
}
}
@@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/postgresql';
import { OrderItem } from '../entities/order-item.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { FindOrderItemsDto } from '../dto/find-order-items.dto';
class FindOrderItemsOpts extends FindOrderItemsDto {
userId?: string;
isPrintFormAdded?: boolean
};
@Injectable()
export class OrderItemRepository extends EntityRepository<OrderItem> {
constructor(readonly em: EntityManager) {
super(em, OrderItem);
}
async findAllPaginated(opts: FindOrderItemsOpts): Promise<PaginatedResult<OrderItem>> {
const {
page = 1,
limit = 10,
search,
orderBy = 'createdAt',
order = 'desc',
userId,
isPrintFormAdded
} = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<OrderItem> = {};
if (userId) {
where.order = { user: { id: userId } };
}
if (isPrintFormAdded != undefined) {
if (isPrintFormAdded == true) {
where.printFormCreatedAt = { $not: null }
} else {
where.printFormCreatedAt = null
}
}
// Search by order number or user information
if (search) {
const searchPattern = `%${search}%`;
const searchConditions: any[] = [
{ user: { firstName: { $ilike: searchPattern } } },
{ user: { lastName: { $ilike: searchPattern } } },
{ user: { phone: { $ilike: searchPattern } } },
];
// If search is numeric, also search by orderNumber
const numericSearch = Number(search);
if (!isNaN(numericSearch) && isFinite(numericSearch)) {
searchConditions.push({ orderNumber: numericSearch });
}
where.$or = searchConditions;
}
// First, fetch orders without reviews
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['designer', 'product'],
});
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}
@@ -0,0 +1,147 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { Order } from '../entities/order.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { OrderStatusEnum } from '../interface/order.interface';
import { FindOrdersDto } from '../dto/find-orders.dto';
class FindOrdersOpts extends FindOrdersDto {
userId?: string;
isInvoiced?: boolean;
designerId?: string
};
@Injectable()
export class OrderRepository extends EntityRepository<Order> {
constructor(readonly em: EntityManager) {
super(em, Order);
}
async findAllPaginated(opts: FindOrdersOpts): Promise<PaginatedResult<Order>> {
const {
page = 1,
limit = 10,
statuses,
search,
orderBy = 'createdAt',
order = 'desc',
userId,
from,
to,
isInvoiced,
designerId
} = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Order> = {};
// Filter by statuses
if (statuses) {
// Ensure statuses is always an array and normalize it
const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses];
// Filter out any empty values
const validStatuses = normalizedStatuses.filter((s): s is OrderStatusEnum => !!s);
if (validStatuses.length > 0) {
// Use direct equality for single value, $in for multiple values to avoid SQL generation issues
if (validStatuses.length === 1) {
where.status = validStatuses[0];
} else {
where.status = { $in: validStatuses };
}
}
}
if (userId) {
where.user = { id: userId };
}
if (isInvoiced != undefined) {
if (isInvoiced == true) {
where.invoicedAt = { $not: null }
} else {
where.invoicedAt = null
}
}
if (designerId) {
where.items = { $some: { designer: { id: designerId } } }
}
// Filter by date range
if (from || to) {
where.createdAt = {};
if (from) {
where.createdAt.$gte = new Date(from);
}
if (to) {
where.createdAt.$lte = new Date(to);
}
}
// Search by order number or user information
if (search) {
const searchPattern = `%${search}%`;
const searchConditions: any[] = [
{ user: { firstName: { $ilike: searchPattern } } },
{ user: { lastName: { $ilike: searchPattern } } },
{ user: { phone: { $ilike: searchPattern } } },
];
// If search is numeric, also search by orderNumber
const numericSearch = Number(search);
if (!isNaN(numericSearch) && isFinite(numericSearch)) {
searchConditions.push({ orderNumber: numericSearch });
}
where.$or = searchConditions;
}
// First, fetch orders without reviews
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['items', 'items.product', 'user'],
});
// Collect all (orderId, productId) pairs for efficient review lookup
// const orderproductPairs: Array<{ orderId: string; productId: bigint }> = [];
// for (const order of data) {
// for (const item of order.items.getItems()) {
// if (item.product?.id) {
// orderproductPairs.push({ orderId: order.id, productId: item.product.id });
// }
// }
// }
// Map reviewIds to products
// for (const order of data) {
// for (const item of order.items.getItems()) {
// if (item.product) {
// const key = `${order.id}-${item.product.id}`;
// // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// const product = item.product as any;
// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
// }
// }
// }
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}