368 lines
12 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
} |