import { Body, ConflictException, Controller, Delete, Get, HttpCode, HttpStatus, NotFoundException, Param, Patch, Post, UseGuards, } from "@nestjs/common"; import { getPrismaClient, withTenantContext, Prisma } 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 { CreateInboundRouteDto, UpdateInboundRouteDto } from "./dto/create-inbound-route.dto"; /** * Rotas de entrada por DID (PHASE 56, docs/INBOUND_ROUTES.md) — achado * real: nenhuma chamada de tronco carregava `b2bcall_tenant_id` até aqui, * então uma chamada de entrada não tinha como saber de qual tenant é. * `didNumber` é @unique GLOBAL de propósito (mesma exceção já aceita em * `Tenant.telephonyDomain`) — por isso o conflito de duplicidade só * aparece no INSERT (a constraint do banco), nunca por uma pré-checagem * cross-tenant: `InboundRoute` tem RLS de verdade (FORCE ROW LEVEL * SECURITY), então uma query sem contexto de tenant não veria a linha de * outro tenant mesmo se tentasse. */ @UseGuards(JwtAuthGuard, PermissionGuard) @Controller("inbound-routes") export class InboundRoutesController { @RequirePermission("inbound_routes.manage") @Post() async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreateInboundRouteDto) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; try { const route = await withTenantContext(prisma, tenantId, (tx) => tx.inboundRoute.create({ data: { tenantId, didNumber: dto.didNumber, description: dto.description, destinationType: dto.destinationType ?? "EXTENSION", destinationContext: dto.destinationContext ?? "default", destinationNumber: dto.destinationNumber, enabled: dto.enabled ?? true, }, }), ); await recordAuditEvent(prisma, { action: "INBOUND_ROUTE_CREATE", tenantId, userId: user.sub, entityType: "inbound_route", entityId: route.id, after: { didNumber: route.didNumber, destinationContext: route.destinationContext, destinationNumber: route.destinationNumber }, }); return route; } catch (err) { if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { throw new ConflictException("Este número (DID) já está em uso por outra rota de entrada"); } throw err; } } @RequirePermission("inbound_routes.view") @Get() async list(@CurrentUser() user: AccessTokenClaims) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; return withTenantContext(prisma, tenantId, (tx) => tx.inboundRoute.findMany({ where: { deletedAt: null }, orderBy: { didNumber: "asc" } }), ); } @RequirePermission("inbound_routes.view") @Get(":id") async get(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const route = await withTenantContext(prisma, tenantId, (tx) => tx.inboundRoute.findFirst({ where: { id, deletedAt: null } }), ); if (!route) throw new NotFoundException(); return route; } @RequirePermission("inbound_routes.manage") @Patch(":id") async update(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: UpdateInboundRouteDto) { const prisma = getPrismaClient(); const tenantId = user.tenantId!; const result = await withTenantContext(prisma, tenantId, (tx) => tx.inboundRoute.updateMany({ where: { id, tenantId, deletedAt: null }, data: { ...(dto.description !== undefined ? { description: dto.description } : {}), ...(dto.destinationType !== undefined ? { destinationType: dto.destinationType } : {}), ...(dto.destinationContext !== undefined ? { destinationContext: dto.destinationContext } : {}), ...(dto.destinationNumber !== undefined ? { destinationNumber: dto.destinationNumber } : {}), ...(dto.enabled !== undefined ? { enabled: dto.enabled } : {}), }, }), ); if (result.count === 0) throw new NotFoundException(); const updated = await withTenantContext(prisma, tenantId, (tx) => tx.inboundRoute.findFirstOrThrow({ where: { id } })); await recordAuditEvent(prisma, { action: "INBOUND_ROUTE_UPDATE", tenantId, userId: user.sub, entityType: "inbound_route", entityId: id, after: { ...dto }, }); return updated; } @RequirePermission("inbound_routes.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.inboundRoute.updateMany({ where: { id, deletedAt: null }, data: { deletedAt: new Date(), enabled: false }, }), ); if (result.count === 0) throw new NotFoundException(); await recordAuditEvent(prisma, { action: "INBOUND_ROUTE_DELETE", tenantId, userId: user.sub, entityType: "inbound_route", entityId: id, }); } }