diff --git a/src/modules/invoices/invoices.module.ts b/src/modules/invoices/invoices.module.ts index 018bb1b..ea88f6b 100755 --- a/src/modules/invoices/invoices.module.ts +++ b/src/modules/invoices/invoices.module.ts @@ -23,7 +23,13 @@ import { WalletsModule } from "../wallets/wallets.module"; @Module({ imports: [ LoggerModule, - BullModule.registerQueue({ name: INVOICE.QUEUE_NAME }), + BullModule.registerQueue({ + name: INVOICE.QUEUE_NAME, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + }, + }), TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]), UsersModule, WalletsModule, diff --git a/src/modules/tickets/constant/index.ts b/src/modules/tickets/constant/index.ts index 0be501a..2c24826 100755 --- a/src/modules/tickets/constant/index.ts +++ b/src/modules/tickets/constant/index.ts @@ -1 +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 * * *", +}; diff --git a/src/modules/tickets/providers/tickets.service.ts b/src/modules/tickets/providers/tickets.service.ts index c1cc968..f7d8c9b 100755 --- a/src/modules/tickets/providers/tickets.service.ts +++ b/src/modules/tickets/providers/tickets.service.ts @@ -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 { DataSource, MoreThanOrEqual, Not, QueryRunner } from "typeorm"; +import { TICKET } from "../constant"; import { ReferralService } from "./referral.service"; import { ParamDto } from "../../../common/DTO/param.dto"; 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"; @Injectable() -export class TicketsService { +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, @@ -44,6 +48,10 @@ export class TicketsService { private readonly supportPlansService: SupportPlansService, ) {} + async onModuleInit() { + await this.scheduleAutoCloseJob(); + } + //******************************** */ async getTicketCategories() { 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"); + } } diff --git a/src/modules/tickets/queue/ticket.processor.ts b/src/modules/tickets/queue/ticket.processor.ts new file mode 100644 index 0000000..5832227 --- /dev/null +++ b/src/modules/tickets/queue/ticket.processor.ts @@ -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(); + } + } +} diff --git a/src/modules/tickets/tickets.module.ts b/src/modules/tickets/tickets.module.ts index c157004..3695bd4 100755 --- a/src/modules/tickets/tickets.module.ts +++ b/src/modules/tickets/tickets.module.ts @@ -1,6 +1,8 @@ +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"; @@ -19,14 +21,18 @@ 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]), + BullModule.registerQueue({ name: TICKET.QUEUE_NAME }), UsersModule, DanakServicesModule, NotificationModule, SupportPlansModule, + LoggerModule, ], providers: [ GroupStrategy, @@ -42,6 +48,7 @@ import { SupportPlansModule } from "../support-plans/support-plans.module"; TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository, + TicketProcessor, ], controllers: [TicketsController], exports: [TicketsService],