Fecha os últimos gaps do módulo Administração (agente.md secao 169): tela de Configurações self-service do próprio tenant (GET/PATCH /tenant-settings, nunca aceita tenantId arbitrário — só user.tenantId das claims), e DELETE /users/:id pra remover alguém do tenant, com duas proteções que não existiam antes (não deixa remover a si mesmo, não deixa remover/rebaixar o último Tenant Admin). Corrige um bug real achado testando a remoção: o delete de TenantMembership (FORCE RLS) rodava dentro de um prisma.$transaction([...]) em forma de array, que nunca seta app.current_tenant_id — Prisma devolvia P2025 "not found" com a linha existindo (500 pro cliente). Mesma classe de bug já corrigida antes em TenantsController.create; corrigido com $transaction(async (tx) => ...) + set_config explícito. Adiciona Discador > Callbacks (GET/PATCH /leads/callbacks, tenant-wide): reagendar, tentar de novo sem esperar, ou desistir de um lead que pediu retorno em outro horário. Remove "Importações" do menu — decisão já registrada na PHASE 39 de não duplicar uma tela pro que já existe (CSV em lote no wizard/detalhe da campanha). Testado ponta a ponta via curl e Puppeteer contra o tenant Acme real: tenant-settings GET/PATCH, proteção de último-admin nos dois endpoints que a usam, convite+remoção de um admin temporário, e o fluxo completo de callback (lead forçado pra CALLBACK via SQL, reagendar rejeitado pro passado/aceito pro futuro, requeue confirmado). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
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<Record<string, unknown>[]> {
|
|
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<Record<string, unknown>> {
|
|
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;
|
|
}
|
|
}
|