chore: add ticket queue
This commit is contained in:
@@ -23,7 +23,13 @@ import { WalletsModule } from "../wallets/wallets.module";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
BullModule.registerQueue({ name: INVOICE.QUEUE_NAME }),
|
BullModule.registerQueue({
|
||||||
|
name: INVOICE.QUEUE_NAME,
|
||||||
|
defaultJobOptions: {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]),
|
TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
WalletsModule,
|
WalletsModule,
|
||||||
|
|||||||
@@ -1 +1,9 @@
|
|||||||
export const REFERRAL_STRATEGY = "referralStrategy";
|
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 * * *",
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
import { InjectQueue } from "@nestjs/bullmq";
|
||||||
|
import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { Queue } from "bullmq";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DataSource, MoreThanOrEqual, Not, QueryRunner } from "typeorm";
|
import { DataSource, MoreThanOrEqual, Not, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
import { TICKET } from "../constant";
|
||||||
import { ReferralService } from "./referral.service";
|
import { ReferralService } from "./referral.service";
|
||||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||||
import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
||||||
@@ -29,10 +32,11 @@ import { TicketMessagesRepository } from "../repositories/tickets-message.reposi
|
|||||||
import { TicketsRepository } from "../repositories/tickets.repository";
|
import { TicketsRepository } from "../repositories/tickets.repository";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TicketsService {
|
export class TicketsService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(TicketsService.name);
|
private readonly logger = new Logger(TicketsService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue,
|
||||||
private readonly ticketsRepository: TicketsRepository,
|
private readonly ticketsRepository: TicketsRepository,
|
||||||
private readonly ticketsCategoryRepository: TicketCategoryRepository,
|
private readonly ticketsCategoryRepository: TicketCategoryRepository,
|
||||||
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
||||||
@@ -44,6 +48,10 @@ export class TicketsService {
|
|||||||
private readonly supportPlansService: SupportPlansService,
|
private readonly supportPlansService: SupportPlansService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.scheduleAutoCloseJob();
|
||||||
|
}
|
||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async getTicketCategories() {
|
async getTicketCategories() {
|
||||||
const ticketCategories = await this.ticketsCategoryRepository.find({});
|
const ticketCategories = await this.ticketsCategoryRepository.find({});
|
||||||
@@ -466,4 +474,17 @@ export class TicketsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//***************************** */
|
||||||
|
|
||||||
|
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,64 @@
|
|||||||
|
import { Processor } from "@nestjs/bullmq";
|
||||||
|
import { Job } from "bullmq";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
|
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||||
|
import { LoggerService } from "../../logger/logger.service";
|
||||||
|
import { TICKET } from "../constant";
|
||||||
|
import { Ticket } from "../entities/ticket.entity";
|
||||||
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
|
||||||
|
@Processor(TICKET.QUEUE_NAME)
|
||||||
|
export class TicketProcessor extends WorkerProcessor {
|
||||||
|
constructor(
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
protected readonly logger: LoggerService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(job: Job, token?: string) {
|
||||||
|
this.logger.log(job);
|
||||||
|
switch (job.name) {
|
||||||
|
case TICKET.AUTO_CLOSE_JOB_NAME:
|
||||||
|
return this.autoCloseInactiveTickets(job, token);
|
||||||
|
default:
|
||||||
|
this.logger.error(`Unknown job name: ${job.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async autoCloseInactiveTickets(job: Job, token?: string) {
|
||||||
|
this.logger.log(`Running auto-close for inactive tickets. ${job.id} ${token ? `Token: ${token}` : ""}`);
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
// Find tickets that are not closed and have no admin/user message in the last 10 days
|
||||||
|
const tickets = await queryRunner.manager.find(Ticket, {
|
||||||
|
where: { status: TicketStatus.PENDING },
|
||||||
|
relations: { messages: true, assignedTo: true, user: true },
|
||||||
|
});
|
||||||
|
const now = dayjs();
|
||||||
|
for (const ticket of tickets) {
|
||||||
|
if (!ticket.messages || ticket.messages.length === 0) continue;
|
||||||
|
// Find the last message
|
||||||
|
const lastMessage = ticket.messages.reduce((a, b) => (dayjs(a.createdAt).isAfter(dayjs(b.createdAt)) ? a : b));
|
||||||
|
// If last message is older than 10 days, close the ticket
|
||||||
|
if (now.diff(dayjs(lastMessage.createdAt), "day") >= TICKET.AUTO_CLOSE_DAYS) {
|
||||||
|
ticket.status = TicketStatus.CLOSED;
|
||||||
|
await queryRunner.manager.save(Ticket, ticket);
|
||||||
|
this.logger.log(`Ticket ${ticket.id} closed due to inactivity.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to auto-close tickets:`, error);
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { TICKET } from "./constant";
|
||||||
import { TicketCategory } from "./entities/ticket-category.entity";
|
import { TicketCategory } from "./entities/ticket-category.entity";
|
||||||
import { TicketMessage } from "./entities/ticket-message.entity";
|
import { TicketMessage } from "./entities/ticket-message.entity";
|
||||||
import { Ticket } from "./entities/ticket.entity";
|
import { Ticket } from "./entities/ticket.entity";
|
||||||
@@ -19,14 +21,18 @@ import { ReferralService } from "./providers/referral.service";
|
|||||||
import { GroupStrategy } from "./strategies/group.strategy";
|
import { GroupStrategy } from "./strategies/group.strategy";
|
||||||
import { RoundRobinStrategy } from "./strategies/round-robin.strategy";
|
import { RoundRobinStrategy } from "./strategies/round-robin.strategy";
|
||||||
import { SupportPlansModule } from "../support-plans/support-plans.module";
|
import { SupportPlansModule } from "../support-plans/support-plans.module";
|
||||||
|
import { TicketProcessor } from "./queue/ticket.processor";
|
||||||
|
import { LoggerModule } from "../logger/logger.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
|
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
|
||||||
|
BullModule.registerQueue({ name: TICKET.QUEUE_NAME }),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
DanakServicesModule,
|
DanakServicesModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
SupportPlansModule,
|
SupportPlansModule,
|
||||||
|
LoggerModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
GroupStrategy,
|
GroupStrategy,
|
||||||
@@ -42,6 +48,7 @@ import { SupportPlansModule } from "../support-plans/support-plans.module";
|
|||||||
TicketCategoryRepository,
|
TicketCategoryRepository,
|
||||||
TicketMessagesRepository,
|
TicketMessagesRepository,
|
||||||
TicketMessageAttachmentsRepository,
|
TicketMessageAttachmentsRepository,
|
||||||
|
TicketProcessor,
|
||||||
],
|
],
|
||||||
controllers: [TicketsController],
|
controllers: [TicketsController],
|
||||||
exports: [TicketsService],
|
exports: [TicketsService],
|
||||||
|
|||||||
Reference in New Issue
Block a user