Files
negareh-api/src/modules/order/providers/order.service.ts
T
2026-07-02 21:31:02 +03:30

368 lines
12 KiB
TypeScript

import { Injectable, BadRequestException, Logger, NotFoundException, ForbiddenException } 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,
} from '../dto/create-order.dto';
import { UserService } from 'src/modules/user/providers/user.service';
import { OrderStatusEnum, IOrderPrintResponse } from '../interface/order.interface';
import { Order } from '../entities/order.entity';
import { AdminService } from 'src/modules/admin/providers/admin.service';
import { UpdateOrderAsAdminDto } from '../dto/update-order.dto';
import { PermissionEnum } from 'src/common/enums/permission.enum';
import { User } from 'src/modules/user/entities/user.entity';
import { Product } from 'src/modules/product/entities/product.entity';
import { Category } from 'src/modules/product/entities/category.entity';
import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { UpdateStatusDto } from '../dto/update-status.dto';
import { CreateOrderPrintDto } from '../dto/create-order-print.dto';
import { OrderPrintValueDto } from '../dto/order-print-value.dto';
import { OrderPrint } from '../entities/order-print.entity';
import { OrderPrintValue } from '../entities/order-print-value.entity';
import { OrderPrintRepository } from '../repositories/order-print.repository';
import { Field } from 'src/modules/form-builder/entities/field.entity';
import { Request } from 'src/modules/request/entities/request.entity';
import { Invoice } from 'src/modules/invoice/entities/invoice.entity';
import { OrderCreatedEvent } from '../events/order.events';
import { InvoiceConfirmStatusEnum } from 'src/modules/invoice/enum/invoice-confirm-status.enum';
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
constructor(
private readonly em: EntityManager,
private readonly orderRepository: OrderRepository,
private readonly userService: UserService,
private readonly adminService: AdminService,
private readonly orderPrintRepository: OrderPrintRepository,
private readonly eventEmitter: EventEmitter2,
) {}
async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto): Promise<Order> {
let invoiceItem: null | InvoiceItem = null
let user: User | null = null
if (dto.invoiceItemId) {
invoiceItem = await this.em.findOne(InvoiceItem, { id: dto.invoiceItemId }, { populate: ['invoice', 'invoice.user', 'product'] });
if (!invoiceItem) {
throw new NotFoundException(`Invoice item with ID ${dto.invoiceItemId} not found.`);
}
if (!invoiceItem.confirmedAt ) {
throw new BadRequestException(`Invoice item is not confirmed`);
}
user = invoiceItem.invoice.user
}
if (!dto.invoiceItemId) {
if (!dto.userId) {
throw new NotFoundException(`UserId is required`);
}
user = await this.userService.findById(dto.userId);
if (!user) {
throw new NotFoundException(`User with ID ${dto.userId} not found.`);
}
}
if (!user) {
throw new NotFoundException(`User not found`);
}
const creator = await this.adminService.findOrFail(adminId);
const type = await this.em.findOne(Category, { id: dto.typeId });
if (!type) {
throw new NotFoundException(`Order type with ID ${dto.typeId} not found.`);
}
let product: Product | null = null;
if (dto.productId) {
product = await this.em.findOne(Product, { id: dto.productId });
if (!product) {
throw new NotFoundException(`Product with ID ${dto.productId} not found.`);
}
} else if (invoiceItem?.product) {
product = invoiceItem.product;
}
let designer = null;
if (dto.designerId) {
designer = await this.adminService.findOrFail(dto.designerId);
}
const order = this.em.create(Order, {
user,
creator,
designer,
type,
product: product ?? null,
title: dto.title,
status: OrderStatusEnum.CREATED,
estimatedDays: dto.estimatedDays,
invoiceItem,
attachments: dto.attachments,
});
await this.em.persistAndFlush(order);
this.eventEmitter.emit(
OrderCreatedEvent.name,
new OrderCreatedEvent(order.id, user.id, order.orderNumber),
);
return order;
}
async findOrdersAsUser(userId: string, dto: FindOrdersDto): Promise<PaginatedResult<Order>> {
return this.orderRepository.findAllPaginated({
userId,
...dto
})
}
async findOrdersAsAdmin(adminId: string, dto: FindOrdersDto): Promise<PaginatedResult<Order>> {
const permissions = await this.adminService.ownedPermissions(adminId,
[PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS])
if (permissions.includes(PermissionEnum.VIEW_ORDERS)) {
return this.orderRepository.findAllPaginated(dto)
} else if (permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS)) {
return this.orderRepository.findAllPaginated({
...dto,
designerId: adminId,
})
} else {
throw new ForbiddenException("You don't have permission to view orders")
}
}
async getOrderAsUser(userId: string, orderId: string): Promise<Order> {
const order = await this.findOrderOrFail(orderId)
if (order.user.id !== userId) {
throw new BadRequestException("this order doesnt belongs to you")
}
return order
}
async updateOrderAsAdmin(id: string, dto: UpdateOrderAsAdminDto): Promise<Order> {
const order = await this.findOrderOrFail(id);
if (dto.userId !== undefined) {
const user = await this.userService.findById(dto.userId);
if (!user) {
throw new NotFoundException(`User with ID ${dto.userId} not found.`);
}
order.user = user as User;
}
if (dto.typeId !== undefined) {
const type = await this.em.findOne(Category, { id: dto.typeId });
if (!type) {
throw new NotFoundException(`Order type with ID ${dto.typeId} not found.`);
}
order.type = type;
}
if (dto.estimatedDays !== undefined) {
order.estimatedDays = dto.estimatedDays;
}
if (dto.title !== undefined) {
order.title = dto.title;
}
if (dto.productId !== undefined) {
if (dto.productId == null || dto.productId === '') {
order.product = null;
} else {
const product = await this.em.findOne(Product, { id: dto.productId });
if (!product) {
throw new NotFoundException(`Product with ID ${dto.productId} not found.`);
}
order.product = product;
}
}
await this.em.flush();
return order;
}
async softDeleteOrder(orderId: string): Promise<void> {
const order = await this.findOrderOrFail(orderId);
order.deletedAt = new Date();
await this.em.flush();
}
async updateStatus(orderId: string, adminId: string, dto: UpdateStatusDto): Promise<Order> {
const permissions = await this.adminService.ownedPermissions(adminId,
[PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS])
if (permissions.includes(PermissionEnum.VIEW_ORDERS)) {
const order = await this.findOrderOrFail(orderId);
order.status = dto.status;
await this.em.flush();
return order
}
if (permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS)) {
//TODO
const order = await this.findOrderOrFailAssignedOrder(orderId, adminId);
order.status = dto.status;
await this.em.flush();
return order
}
throw new ForbiddenException("You don't have permission to update order status")
}
async assignDesigner(orderId: string, designerId: string) {
const order = await this.findOrderOrFail(orderId)
const designer = await this.adminService.findOrFail(designerId)
order.designer = designer
await this.em.flush()
return order
}
async createPrint(orderId: string, dto: CreateOrderPrintDto): Promise<IOrderPrintResponse> {
const order = await this.em.findOne(Order, { id: orderId }, { populate: ['print'] });
if (!order) {
throw new NotFoundException(`Order with ID ${orderId} not found.`);
}
const fieldMap = await this.resolvePrintFields(dto.fields);
let print = order.print;
if (print) {
await this.em.populate(print, ['values']);
print.values.removeAll();
} else {
print = this.em.create(OrderPrint, { order });
}
this.assignPrintValues(print, dto.fields, fieldMap);
await this.em.persistAndFlush(print);
await this.em.populate(print, ['values', 'values.field']);
return this.serializeOrderPrint(print);
}
async getPrint(orderId: string): Promise<IOrderPrintResponse> {
const orderPrint = await this.orderPrintRepository.findOne(
{ order: { id: orderId } },
{ populate: ['values', 'values.field'] },
);
if (!orderPrint) {
throw new NotFoundException(`Print with ID ${orderId} not found.`);
}
return this.serializeOrderPrint(orderPrint);
}
private serializeOrderPrint(print: OrderPrint): IOrderPrintResponse {
return {
id: print.id,
createdAt: print.createdAt,
deletedAt: print.deletedAt,
fileds: print.values.getItems().map((item) => ({
fieldId: item.field.id,
value: item.value,
})),
};
}
private async resolvePrintFields(dtoFields: OrderPrintValueDto[]): Promise<Map<string, Field>> {
const fieldIds = [...new Set(dtoFields.map((field) => field.fieldId))];
const fields = await this.em.find(Field, { id: { $in: fieldIds } });
if (fields.length !== fieldIds.length) {
throw new NotFoundException('One or more fields not found.');
}
return new Map(fields.map((field) => [field.id, field]));
}
private assignPrintValues(
print: OrderPrint,
dtoFields: OrderPrintValueDto[],
fieldMap: Map<string, Field>,
): void {
for (const dtoField of dtoFields) {
print.values.add(
this.em.create(OrderPrintValue, {
orderPrint: print,
field: fieldMap.get(dtoField.fieldId)!,
value: dtoField.value,
}),
);
}
}
// -----------Hrlper Methods -----------
async findOrderOrFail(id: string): Promise<Order> {
const order = await this.em.findOne(Order, { id }, { populate: ['user', 'type', 'product', 'creator', 'designer'] });
if (!order) {
throw new NotFoundException(`Order with ID ${id} not found.`);
}
return order;
}
async findOrderOrFailAssignedOrder(id: string, adminId: string): Promise<Order> {
const order = await this.em.findOne(Order, { id }, { populate: ['user', 'type', 'product', 'creator'] });
if (!order) {
throw new NotFoundException(`Order with ID ${id} not found.`);
}
if (order?.designer?.id !== adminId) {
throw new ForbiddenException("You don't have permission to view this order")
}
return order;
}
async getStats(userId: string) {
const [requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount] =
await Promise.all([
this.em.count(Request, { user: { id: userId } }),
this.em.count(Invoice, { user: { id: userId } }),
this.em.count(Order, {
user: { id: userId },
status: { $nin: [OrderStatusEnum.FINISHED, OrderStatusEnum.CANCELED] },
}),
this.em.count(Order, { user: { id: userId }, status: OrderStatusEnum.FINISHED }),
]);
return {
requestsCount,
invoicesCount,
inProgressOrdersCount,
completedOrdersCount,
};
}
async getAdminStats() {
const [requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount] =
await Promise.all([
this.em.count(Request),
this.em.count(Invoice),
this.em.count(Order, {
status: { $nin: [OrderStatusEnum.FINISHED, OrderStatusEnum.CANCELED] },
}),
this.em.count(Order, { status: OrderStatusEnum.FINISHED }),
]);
return {
requestsCount,
invoicesCount,
inProgressOrdersCount,
completedOrdersCount,
};
}
}