import { BadRequestException, Body, Controller, Get, NotFoundException, Param, Patch, UseGuards } from "@nestjs/common"; import { getPrismaClient, withTenantContext } from "@b2bcall/database"; import { recordAuditEvent, type AccessTokenClaims } from "@b2bcall/auth"; 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 { UpdateCallbackDto } from "./dto/update-callback.dto"; /** * "Discador > Callbacks" (agente.md secao 169) — visão tenant-wide (todas * as campanhas) dos leads que o `PredictiveDialerEngine` marcou como * `CALLBACK` (secao 78-79: agente pediu pra ligar de volta em outro * horário). Antes desta tela não existia forma nenhuma de ver ou agir * sobre esses leads — só ficavam esperando `nextAttemptAt` chegar, sem * ninguém poder adiantar, adiar ou cancelar. */ @UseGuards(JwtAuthGuard, PermissionGuard) @Controller("leads/callbacks") export class CallbacksController { @RequirePermission("campaigns.view") @Get() async list(@CurrentUser() user: AccessTokenClaims): Promise[]> { const prisma = getPrismaClient(); const tenantId = user.tenantId!; return withTenantContext(prisma, tenantId, (tx) => tx.lead.findMany({ where: { tenantId, status: "CALLBACK" }, include: { campaign: { select: { id: true, name: true } } }, orderBy: { nextAttemptAt: "asc" }, take: 500, }), ); } @RequirePermission("campaigns.update") @Patch(":id") async update( @CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: UpdateCallbackDto, ): Promise> { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const lead = await withTenantContext(prisma, tenantId, (tx) => tx.lead.findFirst({ where: { id, tenantId, status: "CALLBACK" } }), ); if (!lead) throw new NotFoundException("Callback nao encontrado (ou o lead ja saiu do status CALLBACK)"); if (dto.action === "RESCHEDULE" && new Date(dto.nextAttemptAt!).getTime() <= Date.now()) { throw new BadRequestException("nextAttemptAt precisa ser no futuro"); } const data = dto.action === "RESCHEDULE" ? { nextAttemptAt: new Date(dto.nextAttemptAt!) } : dto.action === "REQUEUE" ? { status: "READY" as const, nextAttemptAt: null } : { status: "DO_NOT_CALL" as const, nextAttemptAt: null }; const updated = await withTenantContext(prisma, tenantId, (tx) => tx.lead.update({ where: { id }, data })); await recordAuditEvent(prisma, { action: "LEAD_CALLBACK_UPDATE", tenantId, userId: user.sub, entityType: "lead", entityId: id, after: { action: dto.action, nextAttemptAt: dto.nextAttemptAt ?? null }, }); return updated; } }