diff --git a/src/app.module.ts b/src/app.module.ts index 9a6bf29..6f0ad48 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -68,7 +68,6 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; LoggerModule, ], controllers: [], - providers: [], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/modules/tickets/constant/index.ts b/src/modules/tickets/constant/index.ts new file mode 100644 index 0000000..0be501a --- /dev/null +++ b/src/modules/tickets/constant/index.ts @@ -0,0 +1 @@ +export const REFERRAL_STRATEGY = "referralStrategy"; diff --git a/src/modules/tickets/interfaces/referral-strategy.ts b/src/modules/tickets/interfaces/referral-strategy.ts new file mode 100644 index 0000000..3cdbb36 --- /dev/null +++ b/src/modules/tickets/interfaces/referral-strategy.ts @@ -0,0 +1,6 @@ +import { UserGroup } from "../../users/entities/user-group.entity"; +import { User } from "../../users/entities/user.entity"; + +export interface IReferralStrategy { + assignAdmin(group: UserGroup): Promise; +} diff --git a/src/modules/tickets/providers/referral-strategy.factory.ts b/src/modules/tickets/providers/referral-strategy.factory.ts new file mode 100644 index 0000000..9af2c96 --- /dev/null +++ b/src/modules/tickets/providers/referral-strategy.factory.ts @@ -0,0 +1,29 @@ +import { Injectable } from "@nestjs/common"; + +import { IReferralStrategy } from "../interfaces/referral-strategy"; +import { GroupStrategy } from "../strategies/group.strategy"; +import { RoundRobinStrategy } from "../strategies/round-robin.strategy"; + +export enum ReferralStrategyType { + GROUP = "GROUP", + ROUND_ROBIN = "ROUND_ROBIN", +} + +@Injectable() +export class ReferralStrategyFactory { + constructor( + private readonly groupStrategy: GroupStrategy, + private readonly roundRobinStrategy: RoundRobinStrategy, + ) {} + + getStrategy(type: ReferralStrategyType): IReferralStrategy { + switch (type) { + case ReferralStrategyType.GROUP: + return this.groupStrategy; + case ReferralStrategyType.ROUND_ROBIN: + return this.roundRobinStrategy; + default: + return this.groupStrategy; // Default to group strategy + } + } +} diff --git a/src/modules/tickets/providers/referral.service.ts b/src/modules/tickets/providers/referral.service.ts new file mode 100644 index 0000000..722490b --- /dev/null +++ b/src/modules/tickets/providers/referral.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@nestjs/common"; + +import { ReferralStrategyFactory, ReferralStrategyType } from "./referral-strategy.factory"; +import { UserGroup } from "../../users/entities/user-group.entity"; +import { User } from "../../users/entities/user.entity"; +import { IReferralStrategy } from "../interfaces/referral-strategy"; + +@Injectable() +export class ReferralService { + private currentStrategy: IReferralStrategy; + + constructor(private readonly strategyFactory: ReferralStrategyFactory) { + this.currentStrategy = this.strategyFactory.getStrategy(ReferralStrategyType.GROUP); + } + + assignAdmin(group: UserGroup): Promise { + return this.currentStrategy.assignAdmin(group); + } + + setStrategy(type: ReferralStrategyType) { + this.currentStrategy = this.strategyFactory.getStrategy(type); + } +} diff --git a/src/modules/tickets/providers/tickets.service.ts b/src/modules/tickets/providers/tickets.service.ts index 5e52665..d7a0141 100755 --- a/src/modules/tickets/providers/tickets.service.ts +++ b/src/modules/tickets/providers/tickets.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; import { DataSource, 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"; @@ -32,6 +33,7 @@ export class TicketsService { private readonly ticketsRepository: TicketsRepository, private readonly ticketMessagesRepository: TicketMessagesRepository, private readonly usersService: UsersService, + private readonly referralService: ReferralService, private dataSource: DataSource, ) {} //******************************** */ @@ -371,41 +373,26 @@ export class TicketsService { return ticketCount; } - //******************************** */ - async autoAssignUser(category: TicketCategory): Promise { - if (!category.group) return null; - - const { users } = await this.usersService.getUsersByGroup(category.group.id); - - if (users.length === 0) return null; - - users.sort((a, b) => (a.tickets.length || 0) - (b.tickets.length || 0)); - console.log(users); - - return users[0]; - } - //***************************** */ private async assignTicketAndNotify(ticket: Ticket, ticketCategory: TicketCategory, user: User, queryRunner: QueryRunner) { - const assignedUser = await this.autoAssignUser(ticketCategory); + // const assignedUser = await this.autoAssignUser(ticketCategory); + const assignedUser = await this.referralService.assignAdmin(ticketCategory.group); + // + ticket.assignedTo = assignedUser; + await queryRunner.manager.save(Ticket, ticket); - if (assignedUser) { - ticket.assignedTo = assignedUser; - await queryRunner.manager.save(Ticket, ticket); - - await this.notificationsService.createAssignTicketNotificationForAdmin( - assignedUser.id, - { - ticketId: ticket.numericId.toString(), - subject: ticket.subject, - date: ticket.createdAt, - userPhone: user.phone, - userEmail: user.email, - user: `${user.firstName} ${user.lastName}`, - }, - queryRunner, - ); - } + await this.notificationsService.createAssignTicketNotificationForAdmin( + assignedUser.id, + { + ticketId: ticket.numericId.toString(), + subject: ticket.subject, + date: ticket.createdAt, + userPhone: user.phone, + userEmail: user.email, + user: `${user.firstName} ${user.lastName}`, + }, + queryRunner, + ); await this.notificationsService.createTicketNotification( user.id, diff --git a/src/modules/tickets/strategies/group.strategy.ts b/src/modules/tickets/strategies/group.strategy.ts new file mode 100644 index 0000000..b0d18a7 --- /dev/null +++ b/src/modules/tickets/strategies/group.strategy.ts @@ -0,0 +1,39 @@ +import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; + +import { UserGroup } from "../../users/entities/user-group.entity"; +import { User } from "../../users/entities/user.entity"; +import { UsersService } from "../../users/providers/users.service"; +import { IReferralStrategy } from "../interfaces/referral-strategy"; + +@Injectable() +export class GroupStrategy implements IReferralStrategy { + private readonly logger = new Logger(GroupStrategy.name); + + constructor(private readonly usersService: UsersService) {} + + async assignAdmin(group: UserGroup): Promise { + try { + const { users: admins } = await this.usersService.getUsersByGroup(group.id); + + if (!admins.length) { + this.logger.warn(`No admins found in Group: ${group.name}, falling back to super admin`); + //fall back + const superAdmin = await this.usersService.getSuperAdmin(); + if (!superAdmin) throw new InternalServerErrorException("No super admin found as fallback"); + + return superAdmin; + } + + // return the one with the fewest tickets + return admins.reduce((prev, curr) => ((prev.tickets?.length || 0) < (curr.tickets?.length || 0) ? prev : curr)); + // + } catch (error: unknown) { + this.logger.error(`Error assigning admin for group ${group.name}: ${error instanceof Error ? error.message : "Unknown error"}`); + // If any error occurs, try to get a super admin as last resort + const superAdmin = await this.usersService.getSuperAdmin(); + if (!superAdmin) throw new InternalServerErrorException("No super admin found as fallback"); + + return superAdmin; + } + } +} diff --git a/src/modules/tickets/strategies/round-robin.strategy.ts b/src/modules/tickets/strategies/round-robin.strategy.ts new file mode 100644 index 0000000..1586fdb --- /dev/null +++ b/src/modules/tickets/strategies/round-robin.strategy.ts @@ -0,0 +1,44 @@ +import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; + +import { UserGroup } from "../../users/entities/user-group.entity"; +import { User } from "../../users/entities/user.entity"; +import { UsersService } from "../../users/providers/users.service"; +import { IReferralStrategy } from "../interfaces/referral-strategy"; + +@Injectable() +export class RoundRobinStrategy implements IReferralStrategy { + private readonly logger = new Logger(RoundRobinStrategy.name); + private currentIndex: Map = new Map(); + + constructor(private readonly usersService: UsersService) {} + + async assignAdmin(group: UserGroup): Promise { + try { + const { users: admins } = await this.usersService.getUsersByGroup(group.id); + + if (!admins.length) { + this.logger.warn(`No admins found in Group: ${group.name}, falling back to super admin`); + const superAdmin = await this.usersService.getSuperAdmin(); + if (!superAdmin) throw new InternalServerErrorException("No super admin found as fallback"); + return superAdmin; + } + + // Get or initialize the current index for this group + const currentIndex = this.currentIndex.get(group.id) || 0; + + // Calculate the next admin index using modulo to wrap around + const nextIndex = (currentIndex + 1) % admins.length; + + // Update the current index for this group + this.currentIndex.set(group.id, nextIndex); + + // Return the admin at the calculated index + return admins[nextIndex]; + } catch (error: unknown) { + this.logger.error(`Error assigning admin for group ${group.name}: ${error instanceof Error ? error.message : "Unknown error"}`); + const superAdmin = await this.usersService.getSuperAdmin(); + if (!superAdmin) throw new InternalServerErrorException("No super admin found as fallback"); + return superAdmin; + } + } +} diff --git a/src/modules/tickets/tickets.module.ts b/src/modules/tickets/tickets.module.ts index 123ed12..58aa53a 100755 --- a/src/modules/tickets/tickets.module.ts +++ b/src/modules/tickets/tickets.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"; 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"; @@ -14,6 +15,9 @@ 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"; @Module({ imports: [ @@ -22,7 +26,21 @@ import { NotificationModule } from "../notifications/notifications.module"; DanakServicesModule, NotificationModule, ], - providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository], + providers: [ + GroupStrategy, + RoundRobinStrategy, + ReferralStrategyFactory, + { + provide: ReferralService, + useFactory: (strategyFactory: ReferralStrategyFactory) => new ReferralService(strategyFactory), + inject: [ReferralStrategyFactory], + }, + TicketsService, + TicketsRepository, + TicketCategoryRepository, + TicketMessagesRepository, + TicketMessageAttachmentsRepository, + ], controllers: [TicketsController], exports: [TicketsService], }) diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index 3729251..6de84d7 100755 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -337,6 +337,11 @@ export class UsersService { } /************************************************************ */ + async getSuperAdmin() { + const superAdmin = await this.userRepository.findOne({ where: { roles: { name: RoleEnum.SUPER_ADMIN } }, relations: { roles: true } }); + return superAdmin; + } + async isSuperAdmin(adminId: string) { const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);