import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; import dayjs from "dayjs"; import { DataSource, MoreThanOrEqual, Not, QueryRunner } from "typeorm"; import { ReferralService } from "./referral.service"; import { ParamDto } from "../../../common/DTO/param.dto"; import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum"; import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { SupportPlanFeatureKey } from "../../support-plans/enums/support-plan-feature-key.enum"; import { SupportPlansService } from "../../support-plans/providers/support-plans.service"; import { User } from "../../users/entities/user.entity"; import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto"; import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto"; import { CreateTicketDto } from "../DTO/create-ticket.dto"; import { ReferTicketDto } from "../DTO/refer-ticket.dto"; import { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto"; import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto"; import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto"; import { TicketCategory } from "../entities/ticket-category.entity"; import { TicketMessageAttachment } from "../entities/ticket-message-attachment"; import { TicketMessage } from "../entities/ticket-message.entity"; import { Ticket } from "../entities/ticket.entity"; import { TicketStatus } from "../enums/ticket-status.enum"; import { TicketCategoryRepository } from "../repositories/tickets-category.repository"; import { TicketMessagesRepository } from "../repositories/tickets-message.repository"; import { TicketsRepository } from "../repositories/tickets.repository"; @Injectable() export class TicketsService { private readonly logger = new Logger(TicketsService.name); constructor( private readonly ticketsRepository: TicketsRepository, private readonly ticketsCategoryRepository: TicketCategoryRepository, private readonly ticketMessagesRepository: TicketMessagesRepository, private readonly usersService: UsersService, private readonly adminsService: AdminsService, private readonly notificationQueue: NotificationQueue, private readonly referralService: ReferralService, private readonly dataSource: DataSource, private readonly supportPlansService: SupportPlansService, ) {} //******************************** */ async getTicketCategories() { const ticketCategories = await this.ticketsCategoryRepository.find({}); return { ticketCategories, }; } //******************************** */ async getCategoriesList(queryDto: SearchTicketCategoryDto) { const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto); return { categories, count, paginate: true }; } //******************************** */ async createTicketCategory(createDto: CreateTicketCategoryDto) { const { userGroup } = await this.usersService.getUserGroupById(createDto.userGroupId); const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title); if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); const ticketCategory = this.ticketsCategoryRepository.create({ ...createDto, group: userGroup, }); await this.ticketsCategoryRepository.save(ticketCategory); return { message: TicketMessageEnum.CATEGORY_CREATED, }; } //******************************** */ async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto) { const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); if (updateCategoryDto.title) { const { title } = updateCategoryDto; const existCategory = await this.ticketsCategoryRepository.findOneBy({ title, id: Not(paramDto.id), }); if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); } await this.ticketsCategoryRepository.save({ ...category, ...updateCategoryDto, }); return { message: CommonMessage.UPDATE_SUCCESS, }; } //******************************** */ async toggleCategoryStatus(paramDto: ParamDto) { const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); // category.isActive = !category.isActive; await this.ticketsCategoryRepository.save(category); // return { message: CommonMessage.UPDATE_SUCCESS, isActive: category.isActive, }; } //******************************** */ private async checkTicketLimit(userId: string, queryRunner: QueryRunner): Promise { const { userSupportPlan } = await this.supportPlansService.getUserSupportPlanOfUser(userId); if (!userSupportPlan) throw new BadRequestException(TicketMessageEnum.NO_ACTIVE_SUPPORT_PLAN); const ticketLimitFeature = userSupportPlan.supportPlan.features.find( (feature) => feature.featureKey === SupportPlanFeatureKey.TICKET_LIMIT, ); if (!ticketLimitFeature) throw new BadRequestException(TicketMessageEnum.TICKET_LIMIT_NOT_FOUND); const ticketLimit = parseInt(ticketLimitFeature.featureValue); if (isNaN(ticketLimit)) return; const today = dayjs().startOf("day").toDate(); const ticketCount = await queryRunner.manager.count(this.ticketsRepository.target, { where: { user: { id: userId }, createdAt: MoreThanOrEqual(today), }, }); if (ticketCount >= ticketLimit) throw new BadRequestException(TicketMessageEnum.TICKET_LIMIT_EXCEEDED); } //******************************** */ async createTicket(createDto: CreateTicketDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const [user, ticketCategory] = await Promise.all([ this.usersService.findOneByIdWithQueryRunner(userId, queryRunner), queryRunner.manager.findOne(TicketCategory, { where: { id: createDto.categoryId }, relations: { group: { users: true } }, }), ]); if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); await this.checkTicketLimit(userId, queryRunner); let service = null; if (createDto.danakServiceId) { service = await queryRunner.manager.findOneBy(DanakService, { id: createDto.danakServiceId, }); if (!service) throw new BadRequestException(TicketMessageEnum.SERVICE_NOT_FOUND); } const ticketMessage: Partial = { content: createDto.message, author: user, }; if (createDto.attachmentUrls?.length) { ticketMessage.attachments = createDto.attachmentUrls.map((url) => { const attachment = new TicketMessageAttachment(); attachment.attachmentUrl = url; return attachment; }); } const ticket = queryRunner.manager.create(Ticket, { ...createDto, user, category: ticketCategory, messages: [ticketMessage], danakService: service, }); await queryRunner.manager.save(Ticket, ticket); await this.assignTicketAndNotify(ticket, ticketCategory, user, queryRunner); await queryRunner.commitTransaction(); return { message: TicketMessageEnum.CREATED, ticketId: ticket.id, }; } catch (error) { this.logger.error("Error in create ticket transaction", error); await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //******************************** */ async getTickets(queryDto: SearchTicketQueryDto, userId: string) { const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId); return { tickets, count, paginate: true }; } //******************************** */ async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) { const isSuperAdmin = await this.usersService.isSuperAdmin(adminId); const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, isSuperAdmin); return { tickets, count, paginate: true }; } //******************************** */ async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, isAdmin: boolean) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const ticket = await this.findTicket(queryRunner, ticketId, userId, isAdmin); if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED); const isSuperAdmin = await this.usersService.isSuperAdmin(userId); if (isAdmin && !isSuperAdmin && ticket.assignedTo?.id !== userId) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_ASSIGNED_TO_USER); const user = await queryRunner.manager.findOneBy(User, { id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const ticketMessage = queryRunner.manager.create(TicketMessage, { content: createDto.content, author: user, ticket, }); await queryRunner.manager.save(TicketMessage, ticketMessage); if (createDto.attachmentUrls?.length) { await this.saveAttachments(queryRunner, createDto.attachmentUrls, ticketMessage); } this.updateTicketStatus(ticket, isAdmin); await queryRunner.manager.save(Ticket, ticket); if (isAdmin) { await this.sendNotificationForAdminResponse(ticket, user); } await queryRunner.commitTransaction(); return { message: TicketMessageEnum.MESSAGE_CREATED, ticketMessage, }; } catch (error) { await queryRunner.rollbackTransaction(); if (error instanceof HttpException) throw error; throw new InternalServerErrorException(TicketMessageEnum.ERROR_IN_TICKET_MSG_CREATION); } finally { await queryRunner.release(); } } //******************************** */ async getTicketMessages(ticketId: string, userId: string, isAdmin: boolean) { let ticket: null | Ticket = null; // if (isAdmin) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); } if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); const messages = await this.ticketMessagesRepository.findMessagesByTicketId(ticketId); return { ticket, messages, }; } //******************************** */ async closeTicketByUser(ticketId: string, userId: string, isAdmin: boolean) { let ticket: null | Ticket = null; if (isAdmin) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); } if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); ticket.status = TicketStatus.CLOSED; await this.ticketsRepository.save(ticket); return { message: TicketMessageEnum.CLOSED, }; } //******************************** */ async referTicket(ticketId: string, userId: string, referDto: ReferTicketDto) { const queryRunner = this.dataSource.createQueryRunner(); try { await queryRunner.connect(); await queryRunner.startTransaction(); const isSuperAdmin = await this.usersService.isSuperAdmin(userId); // if (!isSuperAdmin) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_ASSIGNED_TO_USER); console.log("isSuperAdmin", isSuperAdmin); const ticket = await this.ticketsRepository.findTicketById(ticketId); if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); const admin = await this.usersService.findOneByIdWithQueryRunner(referDto.adminId, queryRunner); ticket.assignedTo = admin; await queryRunner.manager.save(this.ticketsRepository.target, ticket); await this.notificationQueue.addAssignTicketNotificationForAdmin(admin.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: admin.phone, userEmail: admin.email, user: `${ticket.user.firstName} ${ticket.user.lastName}`, }); await queryRunner.commitTransaction(); return { message: TicketMessageEnum.REFERRED, ticket, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //******************************** */ async getUnreadTickets() { const unreadTickets = await this.ticketsRepository.count({ where: { status: TicketStatus.PENDING }, }); return unreadTickets; } //******************************** */ async countUserTickets(userId: string) { const ticketCount = await this.ticketsRepository.count({ where: { user: { id: userId }, status: Not(TicketStatus.CLOSED) }, }); return ticketCount; } //***************************** */ private async assignTicketAndNotify(ticket: Ticket, ticketCategory: TicketCategory, user: User, queryRunner: QueryRunner) { const assignedUser = await this.referralService.assignAdmin(ticketCategory.group); // ticket.assignedTo = assignedUser; await queryRunner.manager.save(Ticket, ticket); await this.notificationQueue.addAssignTicketNotificationForAdmin(assignedUser.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: user.phone, userEmail: user.email, user: `${user.firstName} ${user.lastName}`, }); await this.notificationQueue.addTicketNotification(user.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: user.phone, userEmail: user.email, }); await this.notifySuperAdminsForTicket(queryRunner, ticket); } //***************************** */ private async findTicket(queryRunner: QueryRunner, ticketId: string, userId: string, isAdmin: boolean): Promise { const ticket = isAdmin ? await queryRunner.manager.findOne(Ticket, { where: { id: ticketId }, relations: { user: true, assignedTo: true }, }) : await queryRunner.manager.findOne(Ticket, { where: { id: ticketId, user: { id: userId } }, relations: { user: true, assignedTo: true }, }); if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); return ticket; } //***************************** */ private updateTicketStatus(ticket: Ticket, isAdmin: boolean): void { if (isAdmin && ticket.status === TicketStatus.PENDING) { ticket.status = TicketStatus.ANSWERED; } else if (!isAdmin && ticket.status === TicketStatus.ANSWERED) { ticket.status = TicketStatus.PENDING; } } //***************************** */ private async saveAttachments(queryRunner: QueryRunner, attachmentUrls: string[], ticketMessage: TicketMessage) { const attachments = attachmentUrls.map((url) => queryRunner.manager.create(TicketMessageAttachment, { attachmentUrl: url, ticketMessage, }), ); await queryRunner.manager.save(TicketMessageAttachment, attachments); } //***************************** */ private async sendNotificationForAdminResponse(ticket: Ticket, admin: User) { await this.notificationQueue.addAnswerTicketNotification(ticket.user.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: admin.phone, userEmail: admin.email, }); } //***************************** */ private async notifySuperAdminsForTicket(queryRunner: QueryRunner, ticket: Ticket) { const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); for (const admin of superAdmins) { await this.notificationQueue.addNewTicketGlobalNotification(admin.id, { ticketId: ticket.numericId.toString(), ticketSubject: ticket.subject, userPhone: admin.phone, userEmail: admin.email, fullName: `${ticket.user.firstName} ${ticket.user.lastName}`, }); } } }