import { Body, Controller, Delete, Get, HttpCode, HttpStatus, NotFoundException, Param, Patch, Post, UseGuards, } from "@nestjs/common"; import { getPrismaClient, withTenantContext } from "@b2bcall/database"; import { recordAuditEvent, type AccessTokenClaims } from "@b2bcall/auth"; import { assertQuota } from "@b2bcall/entitlements"; import { JwtAuthGuard } from "../common/guards/jwt-auth.guard"; import { PermissionGuard } from "../common/guards/permission.guard"; import { RequirePermission } from "../common/decorators/require-permission.decorator"; import { CurrentUser } from "../common/decorators/current-user.decorator"; import { getRedisClient } from "../common/redis"; import { CreateQueueDto, UpdateQueueDto } from "./dto/create-queue.dto"; const QUEUES_SYNC_CHANNEL = "b2bcall:queues:sync"; async function notifyQueuesChanged(): Promise { await getRedisClient().publish(QUEUES_SYNC_CHANNEL, "sync"); } @UseGuards(JwtAuthGuard, PermissionGuard) @Controller("queues") export class QueuesController { @RequirePermission("queues.manage") @Post() async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreateQueueDto) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const activeCount = await withTenantContext(prisma, tenantId, (tx) => tx.queue.count({ where: { tenantId, deletedAt: null } }), ); await assertQuota(tenantId, "maxQueues", activeCount); const queue = await withTenantContext(prisma, tenantId, (tx) => tx.queue.create({ data: { tenantId, name: dto.name, description: dto.description, strategy: dto.strategy ?? "LONGEST_IDLE_AGENT", mohSound: dto.mohSound, announceSound: dto.announceSound, announceFrequency: dto.announceFrequency ?? 0, maxWaitTime: dto.maxWaitTime ?? 0, maxWaitTimeWithNoAgent: dto.maxWaitTimeWithNoAgent ?? 0, agentNoAnswerStatus: dto.agentNoAnswerStatus, tierRulesApply: dto.tierRulesApply ?? false, tierRuleWaitSecond: dto.tierRuleWaitSecond ?? 300, discardAbandonedAfter: dto.discardAbandonedAfter ?? 60, abandonedResumeAllowed: dto.abandonedResumeAllowed ?? false, skipAgentsWithExternalCalls: dto.skipAgentsWithExternalCalls ?? true, recordingEnabled: dto.recordingEnabled ?? false, }, }), ); await recordAuditEvent(prisma, { action: "QUEUE_CREATE", tenantId, userId: user.sub, entityType: "queue", entityId: queue.id, after: { name: queue.name, strategy: queue.strategy }, }); await notifyQueuesChanged(); return queue; } @RequirePermission("queues.view") @Get() async list(@CurrentUser() user: AccessTokenClaims) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; return withTenantContext(prisma, tenantId, (tx) => tx.queue.findMany({ where: { deletedAt: null }, orderBy: { name: "asc" } }), ); } @RequirePermission("queues.view") @Get(":id") async get(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const queue = await withTenantContext(prisma, tenantId, (tx) => tx.queue.findFirst({ where: { id, deletedAt: null } }), ); if (!queue) { throw new NotFoundException(); } return queue; } /** * Achado real reportado pelo usuário: "na fila nao tem opcao de editar * a fila apos a criacao" — só existia create/delete até aqui. */ @RequirePermission("queues.manage") @Patch(":id") async update(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: UpdateQueueDto) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const result = await withTenantContext(prisma, tenantId, (tx) => tx.queue.updateMany({ where: { id, tenantId, deletedAt: null }, data: { ...(dto.name !== undefined ? { name: dto.name } : {}), ...(dto.description !== undefined ? { description: dto.description } : {}), ...(dto.strategy !== undefined ? { strategy: dto.strategy } : {}), ...(dto.mohSound !== undefined ? { mohSound: dto.mohSound } : {}), ...(dto.announceSound !== undefined ? { announceSound: dto.announceSound } : {}), ...(dto.announceFrequency !== undefined ? { announceFrequency: dto.announceFrequency } : {}), ...(dto.maxWaitTime !== undefined ? { maxWaitTime: dto.maxWaitTime } : {}), ...(dto.maxWaitTimeWithNoAgent !== undefined ? { maxWaitTimeWithNoAgent: dto.maxWaitTimeWithNoAgent } : {}), ...(dto.agentNoAnswerStatus !== undefined ? { agentNoAnswerStatus: dto.agentNoAnswerStatus } : {}), ...(dto.tierRulesApply !== undefined ? { tierRulesApply: dto.tierRulesApply } : {}), ...(dto.tierRuleWaitSecond !== undefined ? { tierRuleWaitSecond: dto.tierRuleWaitSecond } : {}), ...(dto.discardAbandonedAfter !== undefined ? { discardAbandonedAfter: dto.discardAbandonedAfter } : {}), ...(dto.abandonedResumeAllowed !== undefined ? { abandonedResumeAllowed: dto.abandonedResumeAllowed } : {}), ...(dto.skipAgentsWithExternalCalls !== undefined ? { skipAgentsWithExternalCalls: dto.skipAgentsWithExternalCalls } : {}), ...(dto.recordingEnabled !== undefined ? { recordingEnabled: dto.recordingEnabled } : {}), }, }), ); if (result.count === 0) { throw new NotFoundException(); } const updated = await withTenantContext(prisma, tenantId, (tx) => tx.queue.findFirstOrThrow({ where: { id } })); await recordAuditEvent(prisma, { action: "QUEUE_UPDATE", tenantId, userId: user.sub, entityType: "queue", entityId: id, after: { ...dto }, }); await notifyQueuesChanged(); return updated; } @RequirePermission("queues.manage") @Delete(":id") @HttpCode(HttpStatus.NO_CONTENT) async remove(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const result = await withTenantContext(prisma, tenantId, (tx) => tx.queue.updateMany({ where: { id, deletedAt: null }, data: { deletedAt: new Date(), enabled: false }, }), ); if (result.count === 0) { throw new NotFoundException(); } await recordAuditEvent(prisma, { action: "QUEUE_DELETE", tenantId, userId: user.sub, entityType: "queue", entityId: id, }); await notifyQueuesChanged(); } }