ticket
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsBoolean, IsNotEmpty, IsString, IsUUID, Length } from "class-validator";
|
||||||
|
|
||||||
|
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
export class CreateTicketCategoryDto {
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
|
||||||
|
@IsString({ message: TicketMessageEnum.TITLE_STRING })
|
||||||
|
@Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
|
||||||
|
@ApiProperty({ description: "Title of the ticket category", example: "فنی", minLength: 3, maxLength: 150 })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.DESCRIPTION_REQUIRED })
|
||||||
|
@IsString({ message: TicketMessageEnum.DESCRIPTION_STRING })
|
||||||
|
@Length(3, 500, { message: TicketMessageEnum.DESCRIPTION_LENGTH })
|
||||||
|
@ApiProperty({ description: "Description of the ticket category", example: "درخواستهای فنی", minLength: 3, maxLength: 500 })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.USER_GROUP_ID_NOT_EMPTY })
|
||||||
|
@IsUUID("4", { message: TicketMessageEnum.USER_GROUP_ID_SHOULD_BE_UUID })
|
||||||
|
@ApiProperty({ description: "User group ID", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||||
|
userGroupId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.IS_ACTIVE_REQUIRED })
|
||||||
|
@IsBoolean({ message: TicketMessageEnum.IS_ACTIVE_SHOULD_BE_BOOLEAN })
|
||||||
|
@ApiProperty({ description: "Is active", example: true })
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { ArrayMinSize, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
|
||||||
|
|
||||||
|
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
export class CreateTicketMessageDto {
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.CONTENT_REQUIRED })
|
||||||
|
@IsString({ message: TicketMessageEnum.CONTENT_STRING })
|
||||||
|
@ApiProperty({ description: "the content of message", example: "test message" })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
|
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
||||||
|
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
}
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { ArrayMinSize, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator";
|
||||||
|
|
||||||
|
import { TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||||
|
|
||||||
|
export class CreateTicketDto {
|
||||||
|
// @IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
|
||||||
|
// @IsString({ message: TicketMessageEnum.TITLE_STRING })
|
||||||
|
// @Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
|
||||||
|
// @ApiProperty({ description: "The ticket title", example: "Test title" })
|
||||||
|
// title: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.SUBJECT_REQUIRED })
|
||||||
|
@IsString({ message: TicketMessageEnum.SUBJECT_STRING })
|
||||||
|
@Length(3, 150, { message: TicketMessageEnum.SUBJECT_LENGTH })
|
||||||
|
@ApiProperty({ description: "Subject of the ticket", example: "Subject" })
|
||||||
|
subject: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.PRIORITY_REQUIRED })
|
||||||
|
@IsEnum(TicketPriority, { message: TicketMessageEnum.PRIORITY_INVALID })
|
||||||
|
@ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" })
|
||||||
|
priority: TicketPriority;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.DANAK_SERVICE_ID_REQUIRED })
|
||||||
|
@IsUUID("4", { message: TicketMessageEnum.DANAK_SERVICE_ID_SHOULD_BE_UUID })
|
||||||
|
@ApiProperty({ description: "Service ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
|
||||||
|
danakServiceId?: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.CATEGORY_ID_REQUIRED })
|
||||||
|
@IsUUID("4", { message: TicketMessageEnum.CATEGORY_ID_SHOULD_BE_UUID })
|
||||||
|
@ApiProperty({ description: "Category ID of the ticket", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||||
|
categoryId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.MESSAGE_REQUIRED })
|
||||||
|
@IsString({ message: TicketMessageEnum.MESSAGE_STRING })
|
||||||
|
@Length(5, 5000, { message: TicketMessageEnum.MESSAGE_LENGTH })
|
||||||
|
@ApiProperty({ description: "The message content of the ticket", example: "This is a detailed description of the issue." })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
|
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
||||||
|
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateTicketByAdminDto extends CreateTicketDto {
|
||||||
|
@IsNotEmpty({ message: UserMessage.USER_ID_REQUIRED })
|
||||||
|
@IsUUID("4", { message: UserMessage.USER_ID_SHOULD_BE_A_UUID })
|
||||||
|
@ApiProperty({ description: "User ID of the ticket", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||||
|
|
||||||
|
export class SearchTicketCategoryDto extends CategoryListSearchQueryDto {}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEnum, IsOptional, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||||
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
|
||||||
|
export class SearchTicketQueryDto extends PaginationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TicketStatus)
|
||||||
|
@ApiPropertyOptional({ enum: TicketStatus, description: "ticket status", example: TicketStatus.PENDING })
|
||||||
|
status?: TicketStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID("4", { message: UserMessage.USER_ID_SHOULD_BE_A_UUID })
|
||||||
|
@ApiPropertyOptional({ description: "user id", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
export const REFERRAL_STRATEGY = "referralStrategy";
|
||||||
|
|
||||||
|
export const TICKET = {
|
||||||
|
QUEUE_NAME: "ticket",
|
||||||
|
AUTO_CLOSE_JOB_NAME: "auto-close-inactive",
|
||||||
|
AUTO_CLOSE_DAYS: 10,
|
||||||
|
AUTO_CLOSE_PRIORITY: 1,
|
||||||
|
AUTO_CLOSE_REPEAT_PATTERN: "0 1 * * *",
|
||||||
|
};
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
Property,
|
||||||
|
Collection,
|
||||||
|
Cascade,
|
||||||
|
Enum,
|
||||||
|
} from '@mikro-orm/core';
|
||||||
|
|
||||||
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { User } from "../../user/entities/user.entity";
|
||||||
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
import { product } from 'src/modules/product/entities/product.entity';
|
||||||
|
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Ticket extends BaseEntity {
|
||||||
|
@Property({ type: "bigint", primary: true })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Enum(() => TicketStatus)
|
||||||
|
status: TicketStatus;
|
||||||
|
// TODO : create relation
|
||||||
|
@Property({type:'json'})
|
||||||
|
products: product | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Admin)
|
||||||
|
admin: Admin | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User,)
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Ticket)
|
||||||
|
parent: Ticket[];
|
||||||
|
|
||||||
|
@Property({ type: 'json' })
|
||||||
|
attachments: string[]
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export enum TicketStatus {
|
||||||
|
PENDING = "PENDING",
|
||||||
|
ANSWERED = "ANSWERED",
|
||||||
|
CLOSED = "CLOSED",
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
+639
@@ -0,0 +1,639 @@
|
|||||||
|
import { InjectQueue } from "@nestjs/bullmq";
|
||||||
|
import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { Queue } from "bullmq";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { DataSource, MoreThanOrEqual, Not, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
import { TICKET } from "../constant";
|
||||||
|
import { ReferralService } from "./referral.service";
|
||||||
|
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator";
|
||||||
|
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||||
|
import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||||
|
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||||
|
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 { CreateTicketByAdminDto, 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 implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(TicketsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue,
|
||||||
|
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,
|
||||||
|
private readonly accessLogService: AccessLogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.scheduleAutoCloseJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
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, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
await this.accessLogService.logCreate("TicketCategory", UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets/categories",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin created ticket category: ${createDto.title}`,
|
||||||
|
metadata: {
|
||||||
|
categoryId: ticketCategory.id,
|
||||||
|
categoryTitle: ticketCategory.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TicketMessageEnum.CATEGORY_CREATED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.accessLogService.logUpdate("TicketCategory", paramDto.id, UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets/categories",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin updated ticket category: ${category.title}`,
|
||||||
|
metadata: {
|
||||||
|
categoryId: category.id,
|
||||||
|
categoryTitle: category.title,
|
||||||
|
},
|
||||||
|
oldValues: JSON.stringify({ ...category }),
|
||||||
|
newValues: JSON.stringify({ ...updateCategoryDto }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//******************************** */
|
||||||
|
async toggleCategoryStatus(paramDto: ParamDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
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);
|
||||||
|
//
|
||||||
|
await this.accessLogService.logUpdate("TicketCategory", paramDto.id, UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets/categories",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin toggled ticket category status: ${category.title}`,
|
||||||
|
metadata: {
|
||||||
|
categoryId: category.id,
|
||||||
|
categoryTitle: category.title,
|
||||||
|
isActive: category.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
|
isActive: category.isActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
|
||||||
|
private async checkTicketLimit(userId: string, queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
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<TicketMessage> = { 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 createTicketByAdmin(createDto: CreateTicketByAdminDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
const [user, admin, ticketCategory] = await Promise.all([
|
||||||
|
this.usersService.findOneByIdWithQueryRunner(createDto.userId, queryRunner),
|
||||||
|
this.usersService.findOneByIdWithQueryRunner(userIpAndHeaders.userId, queryRunner),
|
||||||
|
queryRunner.manager.findOne(TicketCategory, { where: { id: createDto.categoryId }, relations: { group: { users: true } } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||||
|
|
||||||
|
const ticketMessage: Partial<TicketMessage> = { content: createDto.message, author: admin };
|
||||||
|
|
||||||
|
const ticket = queryRunner.manager.create(Ticket, {
|
||||||
|
...createDto,
|
||||||
|
user,
|
||||||
|
category: ticketCategory,
|
||||||
|
messages: [ticketMessage],
|
||||||
|
assignedTo: admin,
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryRunner.manager.save(Ticket, ticket);
|
||||||
|
|
||||||
|
await this.notificationQueue.addTicketNotification(user.id, {
|
||||||
|
ticketId: ticket.numericId.toString(),
|
||||||
|
subject: ticket.subject,
|
||||||
|
date: ticket.createdAt,
|
||||||
|
userPhone: user.phone,
|
||||||
|
userEmail: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.accessLogService.logCreate("Ticket", UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin created ticket: ${ticket.subject}`,
|
||||||
|
metadata: {
|
||||||
|
ticketId: ticket.id,
|
||||||
|
ticketSubject: ticket.subject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TicketMessageEnum.CREATED,
|
||||||
|
ticketId: ticket.id,
|
||||||
|
};
|
||||||
|
} catch (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);
|
||||||
|
const userSupportPlan = await this.supportPlansService.getUserSupportPlanOfUser(ticket.user.id);
|
||||||
|
return {
|
||||||
|
ticket,
|
||||||
|
messages,
|
||||||
|
supportPlan: userSupportPlan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
|
||||||
|
async closeTicketByUser(ticketId: string, isAdmin: boolean, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
let ticket: null | Ticket = null;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||||
|
} else {
|
||||||
|
ticket = await this.ticketsRepository.findTicketById(ticketId, userIpAndHeaders.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
|
|
||||||
|
ticket.status = TicketStatus.CLOSED;
|
||||||
|
await this.ticketsRepository.save(ticket);
|
||||||
|
|
||||||
|
await this.accessLogService.logUpdate("Ticket", ticketId, UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin closed ticket: ${ticket.subject}`,
|
||||||
|
metadata: {
|
||||||
|
ticketId: ticket.id,
|
||||||
|
ticketSubject: ticket.subject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TicketMessageEnum.CLOSED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//******************************** * /
|
||||||
|
async openTicketByAdmin(ticketId: string, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||||
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
|
|
||||||
|
ticket.status = TicketStatus.PENDING;
|
||||||
|
await this.ticketsRepository.save(ticket);
|
||||||
|
|
||||||
|
await this.accessLogService.logUpdate("Ticket", ticketId, UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin opened ticket: ${ticket.subject}`,
|
||||||
|
metadata: {
|
||||||
|
ticketId: ticket.id,
|
||||||
|
ticketSubject: ticket.subject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TicketMessageEnum.OPENED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
async referTicket(ticketId: string, referDto: ReferTicketDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
const isSuperAdmin = await this.usersService.isSuperAdmin(userIpAndHeaders.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();
|
||||||
|
|
||||||
|
await this.accessLogService.logUpdate("Ticket", ticketId, UserType.ADMIN, {
|
||||||
|
user: { id: userIpAndHeaders.userId },
|
||||||
|
endpoint: "/tickets",
|
||||||
|
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||||
|
ipAddress: userIpAndHeaders.ip,
|
||||||
|
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||||
|
actionDescription: `Admin referred ticket: ${ticket.subject}`,
|
||||||
|
metadata: {
|
||||||
|
ticketId: ticket.id,
|
||||||
|
ticketSubject: ticket.subject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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<Ticket> {
|
||||||
|
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}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//***************************** */
|
||||||
|
|
||||||
|
async scheduleAutoCloseJob() {
|
||||||
|
await this.ticketQueue.add(
|
||||||
|
TICKET.AUTO_CLOSE_JOB_NAME,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
repeat: { pattern: TICKET.AUTO_CLOSE_REPEAT_PATTERN },
|
||||||
|
priority: TICKET.AUTO_CLOSE_PRIORITY,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.logger.log("Scheduled auto-close-inactive job for tickets");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { TicketMessage } from "../entities/ticket-message.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketMessagesRepository extends Repository<TicketMessage> {
|
||||||
|
constructor(@InjectRepository(TicketMessage) ticketMessagesRepository: Repository<TicketMessage>) {
|
||||||
|
super(ticketMessagesRepository.target, ticketMessagesRepository.manager, ticketMessagesRepository.queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findMessagesByTicketId(ticketId: string): Promise<TicketMessage[]> {
|
||||||
|
return this.find({
|
||||||
|
relations: {
|
||||||
|
author: {
|
||||||
|
roles: true,
|
||||||
|
},
|
||||||
|
attachments: true,
|
||||||
|
// ticket: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
attachments: {
|
||||||
|
id: true,
|
||||||
|
attachmentUrl: true,
|
||||||
|
},
|
||||||
|
createdAt: true,
|
||||||
|
author: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
roles: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: { ticket: { id: ticketId } },
|
||||||
|
order: { createdAt: "ASC" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||||
|
import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto";
|
||||||
|
import { Ticket } from "../entities/ticket.entity";
|
||||||
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketsRepository extends Repository<Ticket> {
|
||||||
|
constructor(@InjectRepository(Ticket) ticketsRepository: Repository<Ticket>) {
|
||||||
|
super(ticketsRepository.target, ticketsRepository.manager, ticketsRepository.queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTicketById(id: string, userId?: string): Promise<Ticket | null> {
|
||||||
|
return this.findOne({
|
||||||
|
where: { id, ...(userId && { user: { id: userId } }) },
|
||||||
|
relations: {
|
||||||
|
user: true,
|
||||||
|
category: true,
|
||||||
|
assignedTo: true,
|
||||||
|
danakService: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
numericId: true,
|
||||||
|
danakService: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
subject: true,
|
||||||
|
priority: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
user: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
},
|
||||||
|
assignedTo: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************** */
|
||||||
|
|
||||||
|
async findTicketsList(queryDto: SearchTicketQueryDto, userId: string) {
|
||||||
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
|
const queryBuilder = this.createQueryBuilder("ticket")
|
||||||
|
.leftJoinAndSelect("ticket.category", "category")
|
||||||
|
.leftJoinAndSelect("ticket.danakService", "danakService")
|
||||||
|
.where("ticket.user.id = :userId", { userId })
|
||||||
|
.orderBy("ticket.createdAt", "DESC")
|
||||||
|
.select([
|
||||||
|
"ticket.id",
|
||||||
|
"ticket.subject",
|
||||||
|
"ticket.status",
|
||||||
|
"ticket.priority",
|
||||||
|
"ticket.numericId",
|
||||||
|
"ticket.createdAt",
|
||||||
|
"ticket.updatedAt",
|
||||||
|
"category.id",
|
||||||
|
"category.title",
|
||||||
|
"danakService.id",
|
||||||
|
"danakService.name",
|
||||||
|
])
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit);
|
||||||
|
|
||||||
|
if (queryDto.status) {
|
||||||
|
queryBuilder.andWhere("ticket.status = :status", {
|
||||||
|
status: queryDto.status,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
queryBuilder.andWhere("ticket.status != :closedStatus", {
|
||||||
|
closedStatus: TicketStatus.CLOSED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryBuilder.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTicketsListForAdmin(queryDto: SearchTicketQueryDto, adminId: string, isSuperAdmin: boolean) {
|
||||||
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
|
const queryBuilder = this.createQueryBuilder("ticket")
|
||||||
|
.leftJoinAndSelect("ticket.category", "category")
|
||||||
|
.leftJoinAndSelect("ticket.user", "user")
|
||||||
|
.leftJoinAndSelect("ticket.assignedTo", "assignedTo")
|
||||||
|
.leftJoinAndSelect("ticket.danakService", "danakService")
|
||||||
|
.orderBy("ticket.createdAt", "DESC");
|
||||||
|
|
||||||
|
if (!isSuperAdmin) {
|
||||||
|
queryBuilder.andWhere("ticket.assignedToId = :adminId", { adminId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.status) {
|
||||||
|
queryBuilder.andWhere("ticket.status = :status", {
|
||||||
|
status: queryDto.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.userId) {
|
||||||
|
queryBuilder.andWhere("user.id = :userId", { userId: queryDto.userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryBuilder.skip(skip).take(limit).getManyAndCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+140
@@ -0,0 +1,140 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
||||||
|
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
|
||||||
|
import { CreateTicketByAdminDto, 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 { TicketsService } from "./providers/tickets.service";
|
||||||
|
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { PermissionsDec } from "../../common/decorators/permission.decorator";
|
||||||
|
import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
|
||||||
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
|
import { PermissionEnum } from "../users/enums/permission.enum";
|
||||||
|
@Controller("tickets")
|
||||||
|
@ApiTags("Tickets")
|
||||||
|
@AuthGuards()
|
||||||
|
export class TicketsController {
|
||||||
|
constructor(private readonly ticketsService: TicketsService) {}
|
||||||
|
|
||||||
|
//----------------- ticket categories -------------------
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Create ticket category => admin route" })
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Post("category")
|
||||||
|
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Update ticket category => admin route" })
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Patch("category/:id")
|
||||||
|
updateCategory(
|
||||||
|
@Param() paramDto: ParamDto,
|
||||||
|
@Body() updateCategoryDto: UpdateTicketCategoryDto,
|
||||||
|
@UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders,
|
||||||
|
) {
|
||||||
|
return this.ticketsService.updateCategory(paramDto, updateCategoryDto, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get ticket categories " })
|
||||||
|
@Get("categories")
|
||||||
|
getTicketCategories() {
|
||||||
|
return this.ticketsService.getTicketCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get ticket categories list => admin route" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Get("categories-list")
|
||||||
|
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
|
||||||
|
return this.ticketsService.getCategoriesList(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "toggle status of categories => admin route" })
|
||||||
|
@AdminRoute()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Post("category/toggle-status/:id")
|
||||||
|
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------- ticket and ticket messages -------------------
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "create ticket ==> user route" })
|
||||||
|
@Post()
|
||||||
|
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
|
||||||
|
return this.ticketsService.createTicket(createDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "create ticket by admin" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Post("admin")
|
||||||
|
createTicketByAdmin(@Body() createDto: CreateTicketByAdminDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
return this.ticketsService.createTicketByAdmin(createDto, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
|
||||||
|
@Get()
|
||||||
|
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
|
||||||
|
return this.ticketsService.getTickets(queryDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get all tickets ==> admin route" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Get("admin")
|
||||||
|
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
|
||||||
|
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "create ticket messages" })
|
||||||
|
@Post(":id/messages")
|
||||||
|
createTicketMessage(
|
||||||
|
@Param() paramDto: ParamDto,
|
||||||
|
@Body() createDto: CreateTicketMessageDto,
|
||||||
|
@UserDec("id") userId: string,
|
||||||
|
@UserDec("isAdmin") isAdmin: boolean,
|
||||||
|
) {
|
||||||
|
return this.ticketsService.createTicketMessage(paramDto.id, createDto, userId, isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get all ticket messages of user" })
|
||||||
|
@Get(":id/messages")
|
||||||
|
getTicketMessages(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("isAdmin") isAdmin: boolean) {
|
||||||
|
return this.ticketsService.getTicketMessages(paramDto.id, userId, isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "close ticket by user or admin" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post(":id/close")
|
||||||
|
closedTicketByUser(
|
||||||
|
@Param() paramDto: ParamDto,
|
||||||
|
@UserDec("isAdmin") isAdmin: boolean,
|
||||||
|
@UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders,
|
||||||
|
) {
|
||||||
|
return this.ticketsService.closeTicketByUser(paramDto.id, isAdmin, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "open ticket by admin" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Post(":id/open")
|
||||||
|
openTicketByAdmin(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
return this.ticketsService.openTicketByAdmin(paramDto.id, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.TICKETS)
|
||||||
|
@Post(":id/refer")
|
||||||
|
referTicket(@Param() paramDto: ParamDto, @Body() referDto: ReferTicketDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||||
|
return this.ticketsService.referTicket(paramDto.id, referDto, userIpAndHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { TICKET } from "./constant";
|
||||||
|
import { TicketCategory } from "./entities/ticket-category.entity";
|
||||||
|
import { TicketMessage } from "./entities/ticket-message.entity";
|
||||||
|
import { Ticket } from "./entities/ticket.entity";
|
||||||
|
import { ReferralStrategyFactory } from "./providers/referral-strategy.factory";
|
||||||
|
import { TicketsService } from "./providers/tickets.service";
|
||||||
|
import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
|
||||||
|
import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
|
||||||
|
import { TicketsRepository } from "./repositories/tickets.repository";
|
||||||
|
import { TicketsController } from "./tickets.controller";
|
||||||
|
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||||
|
import { UsersModule } from "../users/users.module";
|
||||||
|
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
|
||||||
|
import { TicketMessageAttachmentsRepository } from "./repositories/ticket-message-attachment.repository";
|
||||||
|
import { DanakServicesModule } from "../danak-services/danak-services.module";
|
||||||
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
|
import { ReferralService } from "./providers/referral.service";
|
||||||
|
import { GroupStrategy } from "./strategies/group.strategy";
|
||||||
|
import { RoundRobinStrategy } from "./strategies/round-robin.strategy";
|
||||||
|
import { SupportPlansModule } from "../support-plans/support-plans.module";
|
||||||
|
import { TicketProcessor } from "./queue/ticket.processor";
|
||||||
|
import { LoggerModule } from "../logger/logger.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
|
||||||
|
AccessLogsModule,
|
||||||
|
BullModule.registerQueue({ name: TICKET.QUEUE_NAME }),
|
||||||
|
UsersModule,
|
||||||
|
DanakServicesModule,
|
||||||
|
NotificationModule,
|
||||||
|
SupportPlansModule,
|
||||||
|
LoggerModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
GroupStrategy,
|
||||||
|
RoundRobinStrategy,
|
||||||
|
ReferralStrategyFactory,
|
||||||
|
{
|
||||||
|
provide: ReferralService,
|
||||||
|
useFactory: (strategyFactory: ReferralStrategyFactory) => new ReferralService(strategyFactory),
|
||||||
|
inject: [ReferralStrategyFactory],
|
||||||
|
},
|
||||||
|
TicketsService,
|
||||||
|
TicketsRepository,
|
||||||
|
TicketCategoryRepository,
|
||||||
|
TicketMessagesRepository,
|
||||||
|
TicketMessageAttachmentsRepository,
|
||||||
|
TicketProcessor,
|
||||||
|
],
|
||||||
|
controllers: [TicketsController],
|
||||||
|
exports: [TicketsService],
|
||||||
|
})
|
||||||
|
export class TicketsModule {}
|
||||||
Reference in New Issue
Block a user