DOCS: Adicionado Swagger para documentação de rotas da API
Some checks are pending
Deploy Dev / deploy (push) Waiting to run

This commit is contained in:
Rafael Alves Lopes 2026-05-28 13:37:53 -03:00
parent b2aa1a7712
commit ee966d3306
11 changed files with 631 additions and 2 deletions

94
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@nestjs/core": "^11.1.19",
"@nestjs/platform-express": "^11.1.19",
"@nestjs/platform-socket.io": "^11.1.21",
"@nestjs/swagger": "^11.4.4",
"@nestjs/websockets": "^11.1.21",
"dotenv": "^16.6.1",
"jsonwebtoken": "^9.0.3",
@ -21,6 +22,7 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3",
"swagger-ui-express": "^5.0.1",
"whatsapp-web.js": "^1.34.7",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0"
@ -719,6 +721,12 @@
"node": ">=8"
}
},
"node_modules/@microsoft/tsdoc": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz",
"integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==",
"license": "MIT"
},
"node_modules/@nestjs/cli": {
"version": "11.0.21",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.21.tgz",
@ -852,6 +860,26 @@
}
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz",
"integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.19",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.19.tgz",
@ -917,6 +945,39 @@
}
}
},
"node_modules/@nestjs/swagger": {
"version": "11.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.4.tgz",
"integrity": "sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==",
"license": "MIT",
"dependencies": {
"@microsoft/tsdoc": "0.16.0",
"@nestjs/mapped-types": "2.1.1",
"js-yaml": "4.1.1",
"lodash": "4.18.1",
"path-to-regexp": "8.4.2",
"swagger-ui-dist": "5.32.6"
},
"peerDependencies": {
"@fastify/static": "^8.0.0 || ^9.0.0",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"@fastify/static": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/websockets": {
"version": "11.1.21",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.21.tgz",
@ -1046,6 +1107,13 @@
"node": ">=12"
}
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@so-ric/colorspace": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
@ -3201,6 +3269,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@ -4174,7 +4243,6 @@
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/lodash.includes": {
@ -5990,6 +6058,30 @@
"node": ">=8"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.32.6",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz",
"integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View File

@ -14,6 +14,7 @@
"@nestjs/core": "^11.1.19",
"@nestjs/platform-express": "^11.1.19",
"@nestjs/platform-socket.io": "^11.1.21",
"@nestjs/swagger": "^11.4.4",
"@nestjs/websockets": "^11.1.21",
"dotenv": "^16.6.1",
"jsonwebtoken": "^9.0.3",
@ -23,6 +24,7 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3",
"swagger-ui-express": "^5.0.1",
"whatsapp-web.js": "^1.34.7",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -1,7 +1,14 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@ApiTags('Saude')
@Controller()
export class AppController {
@ApiOperation({
summary: 'Verifica se a API esta online',
description: 'Endpoint simples usado por deploy, monitoramento e testes manuais para confirmar que o backend esta respondendo.',
})
@ApiResponse({ status: 200, description: 'API online.' })
@Get('health')
health() {
return { status: 'ok' };

View File

@ -1,6 +1,7 @@
import 'reflect-metadata';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { json, urlencoded } from 'express';
import { AppModule } from './app.module';
import { loadEnv } from './infra/config/load-env';
@ -30,13 +31,26 @@ async function bootstrap() {
callback(null, true);
return;
}
callback(new Error(`Origem CORS nao permitida: ${origin}`));
Logger.warn(`Origem CORS bloqueada: ${origin}`, 'Cors');
callback(null, false);
},
credentials: true,
});
const swaggerConfig = new DocumentBuilder()
.setTitle('Omnichannel Backend API')
.setDescription('Documentacao das rotas da API do Omnichannel Sothis.')
.setVersion('1.0.0')
.addBearerAuth()
.build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('docs', app, swaggerDocument, {
jsonDocumentUrl: 'docs/openapi.json',
});
await app.listen(port);
Logger.log(`Backend ouvindo na porta ${port}`, 'Bootstrap');
Logger.log(`Swagger disponivel em /docs`, 'Bootstrap');
}
bootstrap();

View File

@ -1,40 +1,90 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AdminAccessService } from './admin-access.service';
@ApiTags('Administracao')
@Controller('admin/access')
export class AdminAccessController {
constructor(private readonly adminAccessService: AdminAccessService) {}
@ApiOperation({
summary: 'Lista opcoes administrativas',
description: 'Retorna dados auxiliares usados nos formularios administrativos, como perfis, areas e opcoes de acesso.',
})
@ApiResponse({ status: 200, description: 'Opcoes administrativas retornadas.' })
@Get('options')
getOptions() {
return this.adminAccessService.getOptions();
}
@ApiOperation({
summary: 'Consulta visao geral operacional',
description: 'Retorna indicadores consolidados para o dashboard administrativo.',
})
@Get('overview')
getOverview() {
return this.adminAccessService.getOverview();
}
@ApiOperation({
summary: 'Consulta ranking de atendentes',
description: 'Lista desempenho de atendentes, opcionalmente filtrando por area.',
})
@ApiQuery({ name: 'areaId', required: false, description: 'ID da area para filtrar o ranking.' })
@Get('ranking')
getRanking(@Query('areaId') areaId?: string) {
return this.adminAccessService.getAttendantRanking(areaId ? Number(areaId) : null);
}
@ApiOperation({
summary: 'Lista auditoria administrativa',
description: 'Retorna eventos de auditoria paginados para rastrear alteracoes e acoes relevantes no painel.',
})
@ApiQuery({ name: 'page', required: false, description: 'Pagina da consulta. Padrao: 1.' })
@ApiQuery({ name: 'limit', required: false, description: 'Quantidade por pagina. Padrao: 100.' })
@Get('audit')
listAuditLogs(@Query('page') page?: string, @Query('limit') limit?: string) {
return this.adminAccessService.listAuditLogs(Number(page || 1), Number(limit || 100));
}
@ApiOperation({
summary: 'Lista conteudos da IA',
description: 'Retorna os documentos e conteudos cadastrados para alimentar a base de conhecimento da IA.',
})
@Get('ai-contents')
listAiContents() {
return this.adminAccessService.listAiContents();
}
@ApiOperation({
summary: 'Baixa arquivo de conteudo da IA',
description: 'Retorna o arquivo original de um conteudo cadastrado na base de conhecimento.',
})
@ApiParam({ name: 'id', description: 'ID do conteudo da IA.' })
@Get('ai-contents/:id/file')
getAiContentFile(@Param('id') id: string) {
return this.adminAccessService.getAiContentFile(Number(id));
}
@ApiOperation({
summary: 'Cria conteudo da IA',
description: 'Cadastra um documento, anotacao ou arquivo para apoiar respostas e contexto da IA.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
title: { type: 'string', example: 'Politica de ferias' },
areaId: { type: 'number', nullable: true, example: 2 },
filename: { type: 'string', nullable: true, example: 'ferias.pdf' },
mimetype: { type: 'string', nullable: true, example: 'application/pdf' },
fileSize: { type: 'number', nullable: true, example: 102400 },
contentBase64: { type: 'string', nullable: true, description: 'Arquivo em base64.' },
notes: { type: 'string', nullable: true, example: 'Regras internas de ferias.' },
createdByUserId: { type: 'number', nullable: true, example: 10 },
},
},
})
@Post('ai-contents')
createAiContent(@Body() body: {
title?: string;
@ -49,21 +99,50 @@ export class AdminAccessController {
return this.adminAccessService.createAiContent(body);
}
@ApiOperation({
summary: 'Remove conteudo da IA',
description: 'Exclui um conteudo cadastrado na base de conhecimento.',
})
@ApiParam({ name: 'id', description: 'ID do conteudo da IA.' })
@Delete('ai-contents/:id')
deleteAiContent(@Param('id') id: string) {
return this.adminAccessService.deleteAiContent(Number(id));
}
@ApiOperation({
summary: 'Lista areas de atendimento',
description: 'Retorna as areas usadas para roteamento, permissoes, especialidades e indicadores.',
})
@Get('areas')
listAreas() {
return this.adminAccessService.listAreas();
}
@ApiOperation({
summary: 'Cria area de atendimento',
description: 'Cadastra uma nova area para roteamento de conversas e organizacao de usuarios.',
})
@ApiBody({
schema: {
type: 'object',
required: ['nome'],
properties: {
nome: { type: 'string', example: 'Recursos Humanos' },
descricao: { type: 'string', nullable: true, example: 'Atendimentos de RH.' },
responsavelUsuarioId: { type: 'number', nullable: true, example: 10 },
},
},
})
@Post('areas')
createArea(@Body() body: { nome: string; descricao?: string | null; responsavelUsuarioId?: number | null }) {
return this.adminAccessService.createArea(body);
}
@ApiOperation({
summary: 'Atualiza area de atendimento',
description: 'Altera dados cadastrais, responsavel ou status de uma area.',
})
@ApiParam({ name: 'id', description: 'ID da area.' })
@Put('areas/:id')
updateArea(
@Param('id') id: string,
@ -72,16 +151,53 @@ export class AdminAccessController {
return this.adminAccessService.updateArea(Number(id), body);
}
@ApiOperation({
summary: 'Remove area de atendimento',
description: 'Exclui ou desativa uma area, conforme regra de negocio aplicada pelo service.',
})
@ApiParam({ name: 'id', description: 'ID da area.' })
@Delete('areas/:id')
deleteArea(@Param('id') id: string) {
return this.adminAccessService.deleteArea(Number(id));
}
@ApiOperation({
summary: 'Lista usuarios',
description: 'Retorna usuarios conhecidos pela administracao para ajuste de perfil, area e especialidades.',
})
@Get('users')
listUsers() {
return this.adminAccessService.listUsers();
}
@ApiOperation({
summary: 'Atualiza acessos de usuario',
description: 'Altera perfil, perfis adicionais, area principal e especialidades de atendimento de um usuario.',
})
@ApiParam({ name: 'id', description: 'ID do usuario.' })
@ApiBody({
schema: {
type: 'object',
properties: {
perfilId: { type: 'number', nullable: true, example: 1 },
perfilIds: { type: 'array', items: { type: 'number' }, example: [1, 2] },
areaId: { type: 'number', nullable: true, example: 2 },
especialidades: {
type: 'array',
items: {
type: 'object',
required: ['areaId'],
properties: {
areaId: { type: 'number', example: 2 },
funcao: { type: 'string', nullable: true, example: 'atendente' },
principal: { type: 'boolean', example: true },
ativo: { type: 'boolean', example: true },
},
},
},
},
},
})
@Put('users/:id')
updateUserAccess(
@Param('id') id: string,

View File

@ -1,20 +1,47 @@
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
import { AgentNotesService } from './agent-notes.service';
@ApiTags('Notas do agente')
@Controller('agent/notes')
export class AgentNotesController {
constructor(private readonly agentNotesService: AgentNotesService) {}
@ApiOperation({
summary: 'Lista notas do agente',
description: 'Retorna lembretes e anotacoes pessoais cadastradas por um agente no painel.',
})
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario dono das notas.' })
@Get()
listNotes(@Query('userId') userId: string) {
return this.agentNotesService.listNotes(Number(userId));
}
@ApiOperation({
summary: 'Cria nota do agente',
description: 'Adiciona uma anotacao pessoal para organizacao do atendente.',
})
@ApiBody({
schema: {
type: 'object',
required: ['userId', 'text'],
properties: {
userId: { type: 'number', example: 10 },
text: { type: 'string', example: 'Retornar contato no fim do dia.' },
},
},
})
@Post()
createNote(@Body() body: { userId: number; text: string }) {
return this.agentNotesService.createNote(Number(body.userId), body.text);
}
@ApiOperation({
summary: 'Remove nota do agente',
description: 'Exclui uma nota pessoal do agente validando o usuario dono da anotacao.',
})
@ApiParam({ name: 'id', description: 'ID da nota.' })
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario dono da nota.' })
@Delete(':id')
deleteNote(@Param('id') id: string, @Query('userId') userId: string) {
return this.agentNotesService.deleteNote(Number(userId), Number(id));

View File

@ -1,30 +1,56 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { AgentPresenceService } from './agent-presence.service';
@ApiTags('Presenca do agente')
@Controller('agent/presence')
export class AgentPresenceController {
constructor(private readonly agentPresenceService: AgentPresenceService) {}
@ApiOperation({
summary: 'Lista presenca dos agentes',
description: 'Retorna o estado atual dos agentes para acompanhamento operacional, incluindo disponibilidade e pausas.',
})
@Get()
listPresence() {
return this.agentPresenceService.listPresence();
}
@ApiOperation({
summary: 'Consulta presenca do agente logado',
description: 'Retorna o status de presenca de um usuario especifico.',
})
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario.' })
@Get('me')
getPresence(@Query('userId') userId: string) {
return this.agentPresenceService.getPresence(Number(userId));
}
@ApiOperation({
summary: 'Coloca agente em pausa',
description: 'Marca o agente como pausado para que ele nao receba novos atendimentos enquanto estiver indisponivel.',
})
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
@Post('pause')
pause(@Body() body: { userId: number }) {
return this.agentPresenceService.pause(Number(body.userId));
}
@ApiOperation({
summary: 'Retoma disponibilidade do agente',
description: 'Remove a pausa do agente e marca o usuario como disponivel novamente.',
})
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
@Post('resume')
resume(@Body() body: { userId: number }) {
return this.agentPresenceService.resume(Number(body.userId));
}
@ApiOperation({
summary: 'Marca agente como offline',
description: 'Atualiza a presenca do agente para offline, usado em saida do painel ou encerramento de expediente.',
})
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
@Post('offline')
offline(@Body() body: { userId: number }) {
return this.agentPresenceService.offline(Number(body.userId));

View File

@ -1,4 +1,5 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CustomerContactsService } from './customer-contacts.service';
interface SaveContactBody {
@ -12,20 +13,51 @@ interface SaveContactBody {
userId?: number | null;
}
@ApiTags('Contatos')
@Controller('contacts')
export class CustomerContactsController {
constructor(private readonly customerContactsService: CustomerContactsService) {}
@ApiOperation({
summary: 'Lista contatos',
description: 'Retorna a agenda de contatos usada para atendimento, abertura ativa e consulta rapida no chat.',
})
@ApiResponse({ status: 200, description: 'Lista de contatos retornada.' })
@Get()
listContacts() {
return this.customerContactsService.listContacts();
}
@ApiOperation({
summary: 'Consulta contato por chat',
description: 'Busca os dados salvos de um contato usando o identificador da conversa.',
})
@ApiParam({ name: 'chatId', description: 'Identificador codificado do chat.' })
@Get(':chatId')
getContact(@Param('chatId') chatId: string) {
return this.customerContactsService.getContact(decodeURIComponent(chatId));
}
@ApiOperation({
summary: 'Salva contato',
description: 'Cria ou atualiza dados do contato associados ao chat, incluindo telefone, email, etiqueta de identificacao e observacao.',
})
@ApiParam({ name: 'chatId', description: 'Identificador codificado do chat.' })
@ApiBody({
schema: {
type: 'object',
properties: {
phone: { type: 'string', nullable: true, example: '5511999999999' },
whatsappPhone: { type: 'string', nullable: true, example: '5511999999999' },
callSmsPhone: { type: 'string', nullable: true, example: '5511988888888' },
email: { type: 'string', nullable: true, example: 'cliente@empresa.com' },
name: { type: 'string', nullable: true, example: 'Tony Silva' },
company: { type: 'string', nullable: true, example: 'Departamento de RH' },
note: { type: 'string', nullable: true, example: 'Contato prefere WhatsApp.' },
userId: { type: 'number', nullable: true, example: 10 },
},
},
})
@Put(':chatId')
saveContact(
@Param('chatId') chatId: string,

View File

@ -1,20 +1,51 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
import { KnowledgeBaseService } from './knowledge-base.service';
@ApiTags('Base de conhecimento')
@Controller('admin/knowledge')
export class KnowledgeBaseController {
constructor(private readonly knowledgeBaseService: KnowledgeBaseService) {}
@ApiOperation({
summary: 'Consulta fluxo do bot',
description: 'Retorna a arvore atual do agente virtual usada para perguntas, opcoes, encerramento e transferencia.',
})
@Get('bot-flow')
getBotFlow() {
return this.knowledgeBaseService.getBotFlow();
}
@ApiOperation({
summary: 'Lista versoes publicadas do fluxo do bot',
description: 'Retorna historico de publicacoes do fluxo para auditoria e acompanhamento de mudancas.',
})
@Get('bot-flow/versions')
listBotFlowVersions() {
return this.knowledgeBaseService.listBotFlowVersions();
}
@ApiOperation({
summary: 'Cria no no fluxo do bot',
description: 'Adiciona uma pergunta, acao de transferencia para agente ou encerramento dentro da arvore do agente virtual.',
})
@ApiBody({
schema: {
type: 'object',
required: ['parentId', 'nodeType'],
properties: {
parentId: { type: 'number', example: 1 },
nodeType: { type: 'string', enum: ['question', 'agent', 'close'], example: 'question' },
title: { type: 'string', example: 'Ferias' },
messageText: { type: 'string', example: 'Voce quer saber sobre saldo ou solicitacao de ferias?' },
keywords: { type: 'string', example: 'ferias, descanso, recesso' },
fallbackMessage: { type: 'string', example: 'Nao entendi. Pode reformular?' },
fallbackAttempts: { type: 'number', example: 2 },
fallbackAreaId: { type: 'number', nullable: true, example: 2 },
areaId: { type: 'number', nullable: true, example: 2 },
},
},
})
@Post('bot-flow/nodes')
createBotFlowNode(@Body() body: {
parentId: number;
@ -30,6 +61,11 @@ export class KnowledgeBaseController {
return this.knowledgeBaseService.createBotFlowNode(body);
}
@ApiOperation({
summary: 'Atualiza no do fluxo do bot',
description: 'Edita textos, palavras-chave, fallback, area e ordenacao de um no existente no agente virtual.',
})
@ApiParam({ name: 'id', description: 'ID do no do fluxo.' })
@Put('bot-flow/nodes/:id')
updateBotFlowNode(@Param('id') id: string, @Body() body: {
title?: string;
@ -44,21 +80,52 @@ export class KnowledgeBaseController {
return this.knowledgeBaseService.updateBotFlowNode(Number(id), body);
}
@ApiOperation({
summary: 'Remove no do fluxo do bot',
description: 'Exclui um no da arvore do agente virtual.',
})
@ApiParam({ name: 'id', description: 'ID do no do fluxo.' })
@Delete('bot-flow/nodes/:id')
deleteBotFlowNode(@Param('id') id: string) {
return this.knowledgeBaseService.deleteBotFlowNode(Number(id));
}
@ApiOperation({
summary: 'Publica fluxo do bot',
description: 'Marca a versao atual do fluxo como publicada para uso pelo agente virtual.',
})
@Post('bot-flow/publish')
publishBotFlow() {
return this.knowledgeBaseService.publishBotFlow();
}
@ApiOperation({
summary: 'Consulta fluxo de triagem',
description: 'Retorna mensagens, publicos, intencoes e regras usadas pelo agente virtual para classificar e rotear o atendimento.',
})
@Get('triage-flow')
getTriageFlow() {
return this.knowledgeBaseService.getTriageFlow();
}
@ApiOperation({
summary: 'Atualiza configuracao do fluxo de triagem',
description: 'Altera mensagens gerais, limite de tentativas e area de fallback do roteamento automatico.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
greetingMessage: { type: 'string', example: 'Ola, sou o Agente Virtual Sothis.' },
audienceQuestion: { type: 'string', example: 'Voce e colaborador, candidato ou cliente?' },
intentQuestionTemplate: { type: 'string', example: 'Sobre qual assunto voce precisa falar?' },
resolutionQuestion: { type: 'string', example: 'Consegui te ajudar?' },
fallbackMessage: { type: 'string', example: 'Vou encaminhar seu atendimento para o time responsavel.' },
fallbackAreaId: { type: 'number', nullable: true, example: 2 },
maxAttempts: { type: 'number', example: 2 },
},
},
})
@Put('triage-flow')
updateTriageFlow(@Body() body: {
greetingMessage?: string;
@ -72,16 +139,29 @@ export class KnowledgeBaseController {
return this.knowledgeBaseService.updateTriageFlow(body);
}
@ApiOperation({
summary: 'Cria publico de triagem',
description: 'Cadastra um publico identificado pelo bot, como colaborador, candidato ou cliente.',
})
@Post('triage-flow/audiences')
createAudience(@Body() body: { label: string; keywords?: string; sortOrder?: number }) {
return this.knowledgeBaseService.createAudience(body);
}
@ApiOperation({
summary: 'Atualiza publico de triagem',
description: 'Edita nome, palavras-chave, ordenacao e status de um publico usado pelo bot.',
})
@ApiParam({ name: 'id', description: 'ID do publico.' })
@Put('triage-flow/audiences/:id')
updateAudience(@Param('id') id: string, @Body() body: { label?: string; keywords?: string; sortOrder?: number; active?: boolean }) {
return this.knowledgeBaseService.updateAudience(Number(id), body);
}
@ApiOperation({
summary: 'Cria intencao de triagem',
description: 'Cadastra um assunto reconhecido pelo bot e associa a intencao a uma area de atendimento.',
})
@Post('triage-flow/intents')
createIntent(@Body() body: {
audienceId: number;
@ -96,6 +176,11 @@ export class KnowledgeBaseController {
return this.knowledgeBaseService.createIntent(body);
}
@ApiOperation({
summary: 'Atualiza intencao de triagem',
description: 'Edita assunto, area, palavras-chave, mensagens e status de uma intencao reconhecida pelo bot.',
})
@ApiParam({ name: 'id', description: 'ID da intencao.' })
@Put('triage-flow/intents/:id')
updateIntent(@Param('id') id: string, @Body() body: {
label?: string;
@ -110,21 +195,50 @@ export class KnowledgeBaseController {
return this.knowledgeBaseService.updateIntent(Number(id), body);
}
@ApiOperation({
summary: 'Lista palavras-chave de roteamento',
description: 'Retorna palavras-chave usadas para identificar area responsavel quando o bot precisa transferir o atendimento.',
})
@ApiQuery({ name: 'areaId', required: false, description: 'ID da area para filtrar as palavras-chave.' })
@Get('routing-keywords')
listRoutingKeywords(@Query('areaId') areaId?: string) {
return this.knowledgeBaseService.listRoutingKeywords(areaId ? Number(areaId) : null);
}
@ApiOperation({
summary: 'Cria palavra-chave de roteamento',
description: 'Associa uma palavra-chave a uma area para melhorar o roteamento automatico do agente virtual.',
})
@ApiBody({
schema: {
type: 'object',
required: ['areaId', 'keyword'],
properties: {
areaId: { type: 'number', example: 2 },
keyword: { type: 'string', example: 'ferias' },
},
},
})
@Post('routing-keywords')
createRoutingKeyword(@Body() body: { areaId: number; keyword: string }) {
return this.knowledgeBaseService.createRoutingKeyword(body);
}
@ApiOperation({
summary: 'Atualiza palavra-chave de roteamento',
description: 'Altera texto ou status de uma palavra-chave usada pelo roteamento automatico.',
})
@ApiParam({ name: 'id', description: 'ID da palavra-chave.' })
@Put('routing-keywords/:id')
updateRoutingKeyword(@Param('id') id: string, @Body() body: { keyword?: string; active?: boolean }) {
return this.knowledgeBaseService.updateRoutingKeyword(Number(id), body);
}
@ApiOperation({
summary: 'Remove palavra-chave de roteamento',
description: 'Exclui uma palavra-chave do mecanismo de roteamento automatico.',
})
@ApiParam({ name: 'id', description: 'ID da palavra-chave.' })
@Delete('routing-keywords/:id')
deleteRoutingKeyword(@Param('id') id: string) {
return this.knowledgeBaseService.deleteRoutingKeyword(Number(id));

View File

@ -1,26 +1,61 @@
import { Body, Controller, Get, Post, Query, Res } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginData } from './auth.types';
@ApiTags('Autenticacao')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({
summary: 'Consulta configuracao publica de login',
description: 'Informa ao frontend quais metodos de autenticacao estao disponiveis para exibir a tela de login corretamente.',
})
@ApiResponse({ status: 200, description: 'Configuracao publica de autenticacao retornada.' })
@Get('config')
getConfig() {
return this.authService.getPublicConfig();
}
@ApiOperation({
summary: 'Realiza login via LDAP/AD',
description: 'Valida as credenciais corporativas no provedor LDAP/Active Directory e retorna os dados do usuario autenticado com token JWT.',
})
@ApiBody({
schema: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string', example: 'rafael.lopes' },
password: { type: 'string', example: 'senha-do-usuario' },
},
},
})
@ApiResponse({ status: 201, description: 'Login realizado com sucesso.' })
@ApiResponse({ status: 401, description: 'Credenciais invalidas ou usuario sem acesso.' })
@Post('login')
login(@Body() body: LoginData) {
return this.authService.loginWithLdap(body);
}
@ApiOperation({
summary: 'Inicia login Microsoft OAuth',
description: 'Redireciona o navegador para a URL de autorizacao da Microsoft quando o login OAuth estiver configurado.',
})
@ApiResponse({ status: 302, description: 'Redirecionamento para Microsoft OAuth.' })
@Get('oauth/microsoft/start')
startMicrosoftLogin(@Res() response: any) {
return response.redirect(this.authService.getMicrosoftAuthorizeUrl());
}
@ApiOperation({
summary: 'Recebe callback Microsoft OAuth',
description: 'Processa o retorno da Microsoft, troca o codigo por usuario autenticado e redireciona o frontend com token e dados do usuario.',
})
@ApiQuery({ name: 'code', required: false, description: 'Codigo de autorizacao retornado pela Microsoft.' })
@ApiQuery({ name: 'state', required: false, description: 'Estado usado para validar a tentativa de login OAuth.' })
@ApiResponse({ status: 302, description: 'Redirecionamento para o frontend com dados de autenticacao.' })
@Get('oauth/microsoft/callback')
async microsoftCallback(@Query() query: { code?: string; state?: string }, @Res() response: any) {
const authResult = await this.authService.loginWithMicrosoftCallback(query);

View File

@ -1,7 +1,9 @@
import { Controller, Get, Post, Body, Delete, Param } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { WhatsappService } from './whatsapp.service';
import { WhatsappAssignmentService } from './whatsapp-assignment.service';
@ApiTags('WhatsApp')
@Controller('whatsapp')
export class WhatsappController {
constructor(
@ -9,86 +11,248 @@ export class WhatsappController {
private readonly assignmentService: WhatsappAssignmentService
) {}
@ApiOperation({
summary: 'Consulta status da conexao WhatsApp',
description: 'Retorna o estado atual da sessao WhatsApp usada pelo backend, como conectado, aguardando QR Code ou desconectado.',
})
@ApiResponse({ status: 200, description: 'Status atual da conexao retornado.' })
@Get('status')
getStatus() {
return { status: this.whatsappService.getStatus() };
}
@ApiOperation({
summary: 'Lista conversas do WhatsApp',
description: 'Retorna os chats conhecidos pelo backend para alimentar o painel de atendimento em tempo real.',
})
@ApiResponse({ status: 200, description: 'Lista de conversas retornada.' })
@Get('chats')
async getChats() {
return this.whatsappService.getChats();
}
@ApiOperation({
summary: 'Lista mensagens de uma conversa',
description: 'Busca o historico de mensagens de um chat especifico para abrir a conversa no painel do atendente.',
})
@ApiParam({ name: 'chatId', description: 'Identificador do chat no WhatsApp.' })
@ApiResponse({ status: 200, description: 'Mensagens do chat retornadas.' })
@Get('messages/:chatId')
async getChatMessages(@Param('chatId') chatId: string) {
return this.whatsappService.getChatMessages(chatId);
}
@ApiOperation({
summary: 'Baixa midia de uma mensagem',
description: 'Retorna a midia associada a uma mensagem do WhatsApp quando a mensagem possui anexo.',
})
@ApiParam({ name: 'chatId', description: 'Identificador do chat no WhatsApp.' })
@ApiParam({ name: 'messageId', description: 'Identificador da mensagem com midia.' })
@ApiResponse({ status: 200, description: 'Midia da mensagem retornada.' })
@Get('media/:chatId/:messageId')
async getMessageMedia(@Param('chatId') chatId: string, @Param('messageId') messageId: string) {
return this.whatsappService.getMessageMedia(chatId, messageId);
}
@ApiOperation({
summary: 'Envia mensagem no WhatsApp',
description: 'Envia uma mensagem ativa para um contato ou responde uma conversa existente, opcionalmente com midia em base64.',
})
@ApiBody({
schema: {
type: 'object',
required: ['to', 'message'],
properties: {
to: { type: 'string', example: '5511999999999@c.us' },
message: { type: 'string', example: 'Ola, como posso ajudar?' },
senderName: { type: 'string', example: 'Rafael Lopes' },
media: {
type: 'object',
nullable: true,
properties: {
data: { type: 'string', description: 'Arquivo em base64.' },
mimetype: { type: 'string', example: 'application/pdf' },
filename: { type: 'string', example: 'documento.pdf' },
},
},
},
},
})
@ApiResponse({ status: 201, description: 'Mensagem enviada.' })
@Post('send')
async sendMessage(@Body() body: { to: string; message: string; senderName?: string; media?: { data: string; mimetype: string; filename?: string } }) {
return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName);
}
@ApiOperation({
summary: 'Inicia atendimento ativo',
description: 'Abre uma conversa ativa usando um template aprovado e registra o atendimento para acompanhamento no painel.',
})
@ApiBody({
schema: {
type: 'object',
required: ['to', 'templateId', 'userId'],
properties: {
to: { type: 'string', example: '5511999999999' },
templateId: { type: 'number', example: 1 },
userId: { type: 'number', example: 10 },
areaId: { type: 'number', nullable: true, example: 2 },
variables: { type: 'object', additionalProperties: { type: 'string', nullable: true } },
},
},
})
@ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' })
@Post('start-attendance')
async startAttendance(@Body() body: { to: string; templateId: number; userId: number; areaId?: number | null; variables?: Record<string, string | null | undefined> }) {
return this.whatsappService.startAttendance(body.to, body.templateId, body.userId, body.areaId, body.variables);
}
@ApiOperation({
summary: 'Assume uma conversa',
description: 'Vincula um chat a um atendente e, opcionalmente, a uma area de atendimento.',
})
@ApiBody({
schema: {
type: 'object',
required: ['chatId', 'userId'],
properties: {
chatId: { type: 'string', example: '5511999999999@c.us' },
userId: { type: 'string', example: '10' },
areaId: { type: 'string', example: '2' },
},
},
})
@Post('assign')
async assignChat(@Body() body: { chatId: string; userId: string; areaId?: string }) {
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
}
@ApiOperation({
summary: 'Transfere uma conversa',
description: 'Encaminha o chat para outra area ou responsavel e registra observacao para contextualizar o proximo atendimento.',
})
@ApiBody({
schema: {
type: 'object',
required: ['chatId', 'areaId'],
properties: {
chatId: { type: 'string', example: '5511999999999@c.us' },
areaId: { type: 'number', example: 2 },
userId: { type: 'number', nullable: true, example: 10 },
note: { type: 'string', nullable: true, example: 'Tony e um colaborador e quer saber sobre ferias.' },
},
},
})
@Post('transfer')
async transferChat(@Body() body: { chatId: string; areaId: number; userId?: number | null; note?: string | null }) {
return this.assignmentService.transferChat(body);
}
@ApiOperation({
summary: 'Libera conversa assumida',
description: 'Remove o vinculo de atendimento do chat, permitindo que outro atendente assuma a conversa.',
})
@ApiParam({ name: 'chatId', description: 'Identificador do chat no WhatsApp.' })
@Delete('release/:chatId')
async releaseChat(@Param('chatId') chatId: string) {
return this.assignmentService.releaseChat(chatId);
}
@ApiOperation({
summary: 'Fecha atendimento',
description: 'Finaliza o atendimento de um chat e remove o vinculo ativo com o atendente.',
})
@ApiBody({
schema: {
type: 'object',
required: ['chatId'],
properties: {
chatId: { type: 'string', example: '5511999999999@c.us' },
userId: { oneOf: [{ type: 'string' }, { type: 'number' }], nullable: true, example: 10 },
},
},
})
@Post('close')
async closeChat(@Body() body: { chatId: string; userId?: string | number | null }) {
return this.assignmentService.closeChat(body.chatId, body.userId);
}
@ApiOperation({
summary: 'Consulta atribuicao do chat',
description: 'Retorna qual atendente ou area esta responsavel pela conversa no momento.',
})
@ApiParam({ name: 'chatId', description: 'Identificador do chat no WhatsApp.' })
@Get('assignment/:chatId')
async getAssignment(@Param('chatId') chatId: string) {
return this.assignmentService.getAssignment(chatId);
}
@ApiOperation({
summary: 'Lista templates de atendimento',
description: 'Retorna os templates cadastrados para abertura ativa e mensagens padronizadas.',
})
@Get('templates')
async getTemplates() {
return this.whatsappService.getTemplates();
}
@ApiOperation({
summary: 'Cria template de mensagem',
description: 'Cadastra um template para abertura ativa ou resposta padronizada. Pode depender de aprovacao conforme o perfil solicitante.',
})
@ApiBody({
schema: {
type: 'object',
required: ['name', 'content'],
properties: {
name: { type: 'string', example: 'Boas vindas' },
content: { type: 'string', example: 'Ola {{nome}}, tudo bem?' },
areaId: { type: 'number', nullable: true, example: 2 },
requestedByRole: { type: 'string', example: 'admin' },
category: { type: 'string', example: 'atendimento' },
},
},
})
@Post('templates')
async saveTemplate(@Body() body: { name: string; content: string; areaId?: number | null; requestedByRole?: string; category?: string }) {
return this.whatsappService.saveTemplate(body.name, body.content, body.areaId, body.requestedByRole, body.category);
}
@ApiOperation({
summary: 'Atualiza template de mensagem',
description: 'Altera nome, conteudo, area ou categoria de um template existente.',
})
@ApiParam({ name: 'id', description: 'ID do template.' })
@Post('templates/update/:id')
async updateTemplate(@Param('id') id: string, @Body() body: { name: string; content: string; areaId?: number | null; category?: string }) {
return this.whatsappService.updateTemplate(Number(id), body.name, body.content, body.areaId, body.category);
}
@ApiOperation({
summary: 'Aprova template pelo admin',
description: 'Marca um template como aprovado para uso nos fluxos de atendimento.',
})
@ApiParam({ name: 'id', description: 'ID do template.' })
@Post('templates/approve-admin/:id')
async approveTemplateByAdmin(@Param('id') id: string) {
return this.whatsappService.approveTemplateByAdmin(Number(id));
}
@ApiOperation({
summary: 'Rejeita template pelo admin',
description: 'Marca um template como rejeitado, impedindo seu uso nos fluxos de atendimento.',
})
@ApiParam({ name: 'id', description: 'ID do template.' })
@Post('templates/reject-admin/:id')
async rejectTemplateByAdmin(@Param('id') id: string) {
return this.whatsappService.rejectTemplateByAdmin(Number(id));
}
@ApiOperation({
summary: 'Remove template',
description: 'Exclui um template cadastrado.',
})
@ApiParam({ name: 'id', description: 'ID do template.' })
@Delete('templates/:id')
async deleteTemplate(@Param('id') id: string) {
return this.whatsappService.deleteTemplate(Number(id));