FEAT: documenta regras do produto e adiciona migrations de suporte

- documenta estado atual, regras de negócio e próximos passos do Omnichannel Sothis
- adiciona migrations para triagem RH, flow builder, auditoria, conteúdos da IA e categoria de templates
- prepara estrutura de banco para fluxo configurável do bot, encerramento automático e templates categorizados
This commit is contained in:
Rafael Alves Lopes 2026-05-26 09:08:53 -03:00
parent a6a09aa740
commit e9e214195a
8 changed files with 509 additions and 14 deletions

View File

@ -11,7 +11,7 @@ O produto hoje permite:
- Login via Active Directory/LDAP e Microsoft OAuth.
- Redirecionamento da Home conforme perfil do usuário.
- Atendimento WhatsApp em tempo real com socket.
- Triagem automática inicial pelo Omnino.
- Triagem automática inicial pelo Agente Virtual Sothis.
- Fila de atendimento por especialidade.
- Assumir, liberar e transferir atendimentos.
- Controle da janela de 24 horas do WhatsApp.
@ -61,7 +61,7 @@ O admin:
- Não precisa pertencer a uma especialidade.
- Vê todas as filas e todos os atendimentos já roteados/classificados.
- Não vê conversas ainda em triagem automática do Omnino (`bot_triage`).
- Não vê conversas ainda em triagem automática do Agente Virtual Sothis (`bot_triage`).
- Cria e gerencia especialidades.
- Define perfis de usuários.
- Pode tornar outro usuário admin.
@ -72,22 +72,22 @@ O admin:
## Regras de Atendimento WhatsApp
### Triagem automática pelo Omnino
### Triagem automática pelo Agente Virtual Sothis
Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Omnino quando ainda não existe atendimento classificado.
Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Agente Virtual Sothis quando ainda não existe atendimento classificado.
Regras atuais:
- Mensagens vazias são registradas, mas não disparam triagem.
- Mídia sem legenda é registrada, mas não aciona o Omnino automaticamente.
- Mídia sem legenda é registrada, mas não aciona o Agente Virtual Sothis automaticamente.
- A triagem é serializada por conversa para evitar respostas duplicadas quando o WhatsApp dispara eventos quase simultâneos.
- O backend guarda `last_routed_message_id` para evitar processar a mesma mensagem mais de uma vez.
Fluxo de decisão:
1. Se a mensagem já contém uma intenção clara, o Omnino roteia direto para a especialidade.
2. Se a primeira mensagem é genérica, o Omnino envia a saudação completa e mantém a conversa em `bot_triage`.
3. Se a segunda mensagem ainda não identifica intenção, o Omnino pede explicitamente: suporte, financeiro ou comercial.
1. Se a mensagem já contém uma intenção clara, o Agente Virtual Sothis roteia direto para a especialidade.
2. Se a primeira mensagem é genérica, o Agente Virtual Sothis envia a saudação completa e mantém a conversa em `bot_triage`.
3. Se a segunda mensagem ainda não identifica intenção, o Agente Virtual Sothis pede explicitamente: suporte, financeiro ou comercial.
4. Se a terceira mensagem ainda for inválida, a conversa cai automaticamente em Suporte.
Exemplos de intenção:
@ -100,7 +100,7 @@ Exemplos de intenção:
A tabela `whatsapp_chat_atribuicoes` representa o estado do atendimento:
- `bot_triage`: conversa ainda está com o Omnino.
- `bot_triage`: conversa ainda está com o Agente Virtual Sothis.
- `queued`: conversa está na fila da especialidade, sem atendente.
- `assigned`: conversa foi assumida ou atribuída diretamente a um atendente.
@ -180,7 +180,7 @@ O frontend bloqueia anexos acima de 15 MB para evitar falhas de payload e degrad
## Chat
A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe mais fallback de conversas mockadas.
A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe fallback local de conversas.
Recursos atuais:
@ -333,7 +333,7 @@ Tela administrativa para usuários, perfis e especialidades.
Regras atuais:
- Usuários vêm do banco/autenticação, não de mock.
- Usuários vêm do banco/autenticação.
- Admin Demo, Supervisor Demo e Atendente Demo foram removidos por migration.
- Admin pode editar um usuário em modal.
- Se o usuário for admin, não há seleção de especialidades.
@ -357,10 +357,10 @@ Recursos atuais:
- Atendentes online simulados.
- Painel do time.
- Fila de espera.
- Atribuição mockada via modal.
- Atribuição via modal.
- Gráfico do dia por hora.
Sempre que há dado real disponível, a tendência do produto é substituir mock por dado real. Indicadores sem origem real consolidada ainda usam dados simulados.
Sempre que há dado operacional disponível, os painéis usam a origem real consolidada. Indicadores sem origem consolidada ainda usam dados de apoio para compor a visão.
## Autenticação
@ -532,7 +532,7 @@ VITE_API_URL=http://localhost:3001
## Regras Importantes Para Desenvolvimento
- Não reintroduzir mock de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp.
- Não reintroduzir fallback local de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp.
- Toda alteração de banco deve virar nova migration numerada.
- Não permitir resposta sem atendimento assumido.
- Não permitir transferência sem atendimento assumido.
@ -542,3 +542,4 @@ VITE_API_URL=http://localhost:3001
- Depois de contato ativo, bloquear novas mensagens livres até resposta do cliente.
- Mídia sem texto não deve enviar cabeçalho solto de atendente.

View File

@ -0,0 +1,99 @@
-- ============================================================
-- Migration 016: Arvore de decisao por especialidade para RH
-- Tabelas:
-- area_routing_keywords
-- ============================================================
CREATE TABLE IF NOT EXISTS area_routing_keywords (
id SERIAL PRIMARY KEY,
area_id INTEGER NOT NULL REFERENCES areas(id) ON DELETE CASCADE,
keyword VARCHAR(160) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_area_routing_keyword UNIQUE (area_id, keyword)
);
CREATE INDEX IF NOT EXISTS idx_area_routing_keywords_area
ON area_routing_keywords (area_id, active);
CREATE INDEX IF NOT EXISTS idx_area_routing_keywords_keyword
ON area_routing_keywords (keyword);
INSERT INTO areas (nome, descricao) VALUES
('Benefícios', 'Duvidas de RH sobre beneficios, convenios, vale transporte e vale refeicao'),
('Ponto', 'Ajustes de ponto, banco de horas, atrasos e jornada'),
('Holerite', 'Holerite, folha de pagamento, descontos e demonstrativos'),
('Férias', 'Ferias, abono, programacao e saldo de descanso'),
('Recrutamento', 'Vagas internas, candidatura, entrevista e processo seletivo')
ON CONFLICT (nome) DO UPDATE SET
descricao = EXCLUDED.descricao,
ativo = TRUE,
updated_at = NOW();
INSERT INTO area_routing_keywords (area_id, keyword)
SELECT a.id, keyword
FROM areas a
JOIN (
VALUES
('Benefícios', 'beneficio'),
('Benefícios', 'beneficios'),
('Benefícios', 'vale refeicao'),
('Benefícios', 'vale alimentacao'),
('Benefícios', 'vale transporte'),
('Benefícios', 'convenio'),
('Benefícios', 'plano de saude'),
('Benefícios', 'odonto'),
('Ponto', 'ponto'),
('Ponto', 'espelho de ponto'),
('Ponto', 'banco de horas'),
('Ponto', 'atraso'),
('Ponto', 'jornada'),
('Ponto', 'batida'),
('Ponto', 'marcacao'),
('Holerite', 'holerite'),
('Holerite', 'folha'),
('Holerite', 'pagamento'),
('Holerite', 'salario'),
('Holerite', 'desconto'),
('Holerite', 'demonstrativo'),
('Férias', 'ferias'),
('Férias', 'abono'),
('Férias', 'descanso'),
('Férias', 'saldo de ferias'),
('Férias', 'programar ferias'),
('Recrutamento', 'vaga'),
('Recrutamento', 'vagas'),
('Recrutamento', 'processo seletivo'),
('Recrutamento', 'entrevista'),
('Recrutamento', 'curriculo'),
('Recrutamento', 'candidatura')
) AS seed(area_nome, keyword) ON seed.area_nome = a.nome
ON CONFLICT (area_id, keyword) DO UPDATE SET
active = TRUE,
updated_at = CURRENT_TIMESTAMP;
INSERT INTO whatsapp_templates (name, content, area_id, status, requested_by_role, admin_approved_at, meta_submitted_at, meta_approved_at, updated_at)
SELECT template.name, template.content, a.id, 'approved', 'admin', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM areas a
JOIN (
VALUES
('rh_abertura_beneficios', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Recebemos sua solicitacao sobre beneficios e vamos te apoiar por aqui.'),
('rh_abertura_ponto', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te ajudar com sua solicitacao sobre ponto, jornada ou banco de horas.'),
('rh_abertura_holerite', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te ajudar com holerite, folha ou demonstrativo de pagamento.'),
('rh_abertura_ferias', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te apoiar com sua solicitacao sobre ferias.'),
('rh_abertura_recrutamento', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te orientar sobre vagas, candidatura ou processo seletivo.')
) AS template(name, content) ON template.name LIKE
CASE a.nome
WHEN 'Benefícios' THEN '%beneficios'
WHEN 'Ponto' THEN '%ponto'
WHEN 'Holerite' THEN '%holerite'
WHEN 'Férias' THEN '%ferias'
WHEN 'Recrutamento' THEN '%recrutamento'
END
ON CONFLICT (name) DO UPDATE SET
content = EXCLUDED.content,
area_id = EXCLUDED.area_id,
status = 'approved',
meta_approved_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP;

View File

@ -0,0 +1,145 @@
-- ============================================================
-- Migration 017: Fluxo configuravel de triagem do Agente Virtual Sothis
-- Tabelas:
-- bot_triage_flows
-- bot_triage_audiences
-- bot_triage_intents
-- whatsapp_chat_atribuicoes
-- ============================================================
CREATE TABLE IF NOT EXISTS bot_triage_flows (
id SERIAL PRIMARY KEY,
name VARCHAR(160) NOT NULL UNIQUE,
active BOOLEAN NOT NULL DEFAULT TRUE,
greeting_message TEXT NOT NULL,
audience_question TEXT NOT NULL,
intent_question_template TEXT NOT NULL,
fallback_message TEXT NOT NULL,
fallback_area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
max_attempts INTEGER NOT NULL DEFAULT 2,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS bot_triage_audiences (
id SERIAL PRIMARY KEY,
flow_id INTEGER NOT NULL REFERENCES bot_triage_flows(id) ON DELETE CASCADE,
label VARCHAR(160) NOT NULL,
keywords TEXT,
sort_order INTEGER NOT NULL DEFAULT 1,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS bot_triage_intents (
id SERIAL PRIMARY KEY,
audience_id INTEGER NOT NULL REFERENCES bot_triage_audiences(id) ON DELETE CASCADE,
label VARCHAR(160) NOT NULL,
area_id INTEGER NOT NULL REFERENCES areas(id) ON DELETE CASCADE,
keywords TEXT,
sort_order INTEGER NOT NULL DEFAULT 1,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS triage_flow_id INTEGER REFERENCES bot_triage_flows(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS triage_audience_id INTEGER REFERENCES bot_triage_audiences(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS triage_step VARCHAR(40);
CREATE INDEX IF NOT EXISTS idx_bot_triage_flows_active
ON bot_triage_flows (active);
CREATE INDEX IF NOT EXISTS idx_bot_triage_audiences_flow
ON bot_triage_audiences (flow_id, active, sort_order);
CREATE INDEX IF NOT EXISTS idx_bot_triage_intents_audience
ON bot_triage_intents (audience_id, active, sort_order);
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_triage_flow
ON whatsapp_chat_atribuicoes (triage_flow_id, triage_audience_id, triage_step);
INSERT INTO areas (nome, descricao) VALUES
('Documentos RH', 'Documentos para ex-colaboradores, informe de rendimentos e comprovantes'),
('Rescisao', 'Rescisao, FGTS, verbas rescisorias e encerramento de contrato')
ON CONFLICT (nome) DO UPDATE SET
descricao = EXCLUDED.descricao,
ativo = TRUE,
updated_at = NOW();
INSERT INTO bot_triage_flows (
name,
active,
greeting_message,
audience_question,
intent_question_template,
fallback_message,
fallback_area_id,
max_attempts,
updated_at
)
SELECT
'RH CAOA - Atendimento Sothis',
TRUE,
'Ola! Sou o Agente Virtual Sothis. Vou te direcionar para o atendimento correto de RH.',
'Digite o numero da opcao que melhor descreve voce:',
'Perfeito. Agora digite o numero do assunto que voce precisa:',
'Nao consegui identificar com seguranca. Vou encaminhar seu atendimento para o suporte de RH.',
a.id,
2,
CURRENT_TIMESTAMP
FROM areas a
WHERE a.nome = 'Suporte'
ON CONFLICT (name) DO UPDATE SET
active = TRUE,
greeting_message = EXCLUDED.greeting_message,
audience_question = EXCLUDED.audience_question,
intent_question_template = EXCLUDED.intent_question_template,
fallback_message = EXCLUDED.fallback_message,
fallback_area_id = EXCLUDED.fallback_area_id,
max_attempts = EXCLUDED.max_attempts,
updated_at = CURRENT_TIMESTAMP;
WITH flow AS (
SELECT id FROM bot_triage_flows WHERE name = 'RH CAOA - Atendimento Sothis'
)
INSERT INTO bot_triage_audiences (flow_id, label, keywords, sort_order, active, updated_at)
SELECT flow.id, seed.label, seed.keywords, seed.sort_order, TRUE, CURRENT_TIMESTAMP
FROM flow
JOIN (
VALUES
('Sou colaborador ativo', 'colaborador, funcionario, matricula, holerite, ferias, ponto, beneficios', 1),
('Sou ex-colaborador', 'ex colaborador, ex-colaborador, rescisao, fgts, informe de rendimentos, desligamento', 2),
('Sou candidato a uma vaga', 'candidato, vaga, curriculo, processo seletivo, entrevista, candidatura', 3)
) AS seed(label, keywords, sort_order) ON TRUE
ON CONFLICT DO NOTHING;
WITH audiences AS (
SELECT bta.id, bta.label
FROM bot_triage_audiences bta
INNER JOIN bot_triage_flows btf ON btf.id = bta.flow_id
WHERE btf.name = 'RH CAOA - Atendimento Sothis'
),
seed AS (
SELECT * FROM (
VALUES
('Sou colaborador ativo', 'Beneficios', 'beneficios, vale refeicao, vale transporte, convenio, plano de saude', 'Benefícios', 1),
('Sou colaborador ativo', 'Holerite', 'holerite, folha, salario, pagamento, desconto', 'Holerite', 2),
('Sou colaborador ativo', 'Ferias', 'ferias, abono, descanso, saldo de ferias', 'Férias', 3),
('Sou colaborador ativo', 'Ponto', 'ponto, espelho de ponto, banco de horas, jornada, batida', 'Ponto', 4),
('Sou ex-colaborador', 'Documentos', 'documento, documentos, comprovante, informe de rendimentos', 'Documentos RH', 1),
('Sou ex-colaborador', 'FGTS e rescisao', 'fgts, rescisao, verbas rescisorias, desligamento', 'Rescisao', 2),
('Sou ex-colaborador', 'Informe de rendimentos', 'informe, rendimentos, imposto de renda, ir', 'Documentos RH', 3),
('Sou candidato a uma vaga', 'Status da vaga', 'status, vaga, retorno, resultado', 'Recrutamento', 1),
('Sou candidato a uma vaga', 'Reagendar entrevista', 'reagendar, entrevista, agenda, horario', 'Recrutamento', 2),
('Sou candidato a uma vaga', 'Nova candidatura', 'nova candidatura, curriculo, candidatar, oportunidade', 'Recrutamento', 3)
) AS rows(audience_label, label, keywords, area_nome, sort_order)
)
INSERT INTO bot_triage_intents (audience_id, label, area_id, keywords, sort_order, active, updated_at)
SELECT audiences.id, seed.label, areas.id, seed.keywords, seed.sort_order, TRUE, CURRENT_TIMESTAMP
FROM seed
INNER JOIN audiences ON audiences.label = seed.audience_label
INNER JOIN areas ON areas.nome = seed.area_nome
ON CONFLICT DO NOTHING;

View File

@ -0,0 +1,56 @@
-- ============================================================
-- Migration 018: Etapa de resposta antes da escalacao para fila
-- Tabelas:
-- bot_triage_flows
-- bot_triage_intents
-- whatsapp_chat_atribuicoes
-- ============================================================
ALTER TABLE bot_triage_flows
ADD COLUMN IF NOT EXISTS resolution_question TEXT NOT NULL DEFAULT 'Essa informacao resolveu sua duvida? Responda 1 para encerrar ou 2 para falar com um especialista.';
ALTER TABLE bot_triage_intents
ADD COLUMN IF NOT EXISTS response_message TEXT,
ADD COLUMN IF NOT EXISTS resolution_question TEXT,
ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para um especialista no assunto.';
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS triage_intent_id INTEGER REFERENCES bot_triage_intents(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_triage_intent
ON whatsapp_chat_atribuicoes (triage_intent_id, triage_step);
UPDATE bot_triage_intents
SET response_message = CASE label
WHEN 'Beneficios' THEN 'Para consultar beneficios, verifique no portal de RH a area Beneficios. La voce encontra vale refeicao, vale transporte, convenio medico, odontologico e regras de elegibilidade.'
WHEN 'Holerite' THEN 'Seu holerite fica disponivel no portal de RH na area Folha de pagamento. Normalmente ele pode ser consultado por competencia, com detalhamento de salario, descontos e beneficios.'
WHEN 'Ferias' THEN 'Para ferias, consulte saldo e periodos disponiveis no portal de RH. A solicitacao precisa respeitar a politica interna e o fluxo de aprovacao do gestor.'
WHEN 'Ponto' THEN 'Para ponto, confira o espelho de ponto e solicite correcao de batida quando necessario. Ajustes de jornada e banco de horas seguem aprovacao do gestor.'
WHEN 'Documentos' THEN 'Documentos de ex-colaborador podem ser solicitados pelo canal de RH. Informe o documento desejado e mantenha seus dados de contato atualizados.'
WHEN 'FGTS e rescisao' THEN 'Para FGTS e rescisao, o time de RH valida o status do desligamento, prazos legais e documentos pendentes antes de retornar.'
WHEN 'Informe de rendimentos' THEN 'O informe de rendimentos e disponibilizado conforme calendario fiscal. Caso nao encontre, o RH pode apoiar com a segunda via.'
WHEN 'Status da vaga' THEN 'Para status de vaga, acompanhe o processo seletivo pelo canal informado na candidatura. O RH pode consultar a etapa atual quando houver identificacao do candidato.'
WHEN 'Reagendar entrevista' THEN 'Para reagendar entrevista, informe nome completo, vaga e melhor disponibilidade. O time de recrutamento avalia a agenda.'
WHEN 'Nova candidatura' THEN 'Para nova candidatura, acesse o portal de vagas e mantenha seu curriculo atualizado. O RH pode orientar sobre oportunidades abertas.'
ELSE COALESCE(response_message, 'Tenho uma orientacao inicial para esse assunto. Se ainda precisar de ajuda, posso encaminhar para um especialista.')
END
WHERE response_message IS NULL;
UPDATE bot_triage_intents
SET resolution_question = 'Isso resolveu sua duvida? Responda 1 para encerrar ou 2 para falar com um especialista de RH.'
WHERE resolution_question IS NULL;
UPDATE bot_triage_intents
SET escalation_message = CASE label
WHEN 'Beneficios' THEN 'Certo, vou te encaminhar para um especialista em beneficios.'
WHEN 'Holerite' THEN 'Certo, vou te encaminhar para um especialista em holerite e folha.'
WHEN 'Ferias' THEN 'Certo, vou te encaminhar para um especialista em ferias.'
WHEN 'Ponto' THEN 'Certo, vou te encaminhar para um especialista em ponto.'
WHEN 'Documentos' THEN 'Certo, vou te encaminhar para um especialista em documentos de RH.'
WHEN 'FGTS e rescisao' THEN 'Certo, vou te encaminhar para um especialista em rescisao.'
WHEN 'Informe de rendimentos' THEN 'Certo, vou te encaminhar para um especialista em documentos de RH.'
WHEN 'Status da vaga' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
WHEN 'Reagendar entrevista' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
WHEN 'Nova candidatura' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
ELSE escalation_message
END;

View File

@ -0,0 +1,127 @@
-- ============================================================
-- Migration 019: Flow Builder visual do bot
-- Tabelas:
-- bot_flow_versions
-- bot_flow_nodes
-- whatsapp_chat_atribuicoes
-- ============================================================
CREATE TABLE IF NOT EXISTS bot_flow_versions (
id SERIAL PRIMARY KEY,
name VARCHAR(160) NOT NULL DEFAULT 'Fluxo RH Sothis',
status VARCHAR(30) NOT NULL DEFAULT 'draft',
version_number INTEGER NOT NULL DEFAULT 0,
root_node_id INTEGER,
published_at TIMESTAMP WITH TIME ZONE,
snapshot JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS bot_flow_nodes (
id SERIAL PRIMARY KEY,
version_id INTEGER NOT NULL REFERENCES bot_flow_versions(id) ON DELETE CASCADE,
parent_id INTEGER REFERENCES bot_flow_nodes(id) ON DELETE CASCADE,
node_type VARCHAR(30) NOT NULL CHECK (node_type IN ('greeting', 'question', 'agent', 'close')),
title VARCHAR(160) NOT NULL,
message_text TEXT,
keywords TEXT,
fallback_message TEXT,
fallback_attempts INTEGER NOT NULL DEFAULT 2,
fallback_area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
sort_order INTEGER NOT NULL DEFAULT 1,
position_x INTEGER NOT NULL DEFAULT 0,
position_y INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_flow_versions_root'
) THEN
ALTER TABLE bot_flow_versions
ADD CONSTRAINT fk_bot_flow_versions_root
FOREIGN KEY (root_node_id) REFERENCES bot_flow_nodes(id) ON DELETE SET NULL;
END IF;
END $$;
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS triage_builder_version_id INTEGER REFERENCES bot_flow_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS triage_builder_node_id INTEGER REFERENCES bot_flow_nodes(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_bot_flow_versions_status
ON bot_flow_versions (status, published_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_bot_flow_nodes_version_parent
ON bot_flow_nodes (version_id, parent_id, sort_order, id);
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_builder_node
ON whatsapp_chat_atribuicoes (triage_builder_version_id, triage_builder_node_id);
WITH inserted_version AS (
INSERT INTO bot_flow_versions (name, status, version_number, updated_at)
SELECT 'Fluxo RH Sothis', 'draft', 0, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM bot_flow_versions WHERE status = 'draft')
RETURNING id
),
draft_version AS (
SELECT id FROM inserted_version
UNION ALL
SELECT id
FROM (
SELECT id
FROM bot_flow_versions
WHERE status = 'draft'
AND NOT EXISTS (SELECT 1 FROM inserted_version)
ORDER BY id ASC
LIMIT 1
) existing_draft
),
inserted_root AS (
INSERT INTO bot_flow_nodes (
version_id,
parent_id,
node_type,
title,
message_text,
keywords,
fallback_message,
fallback_attempts,
sort_order,
position_x,
position_y,
updated_at
)
SELECT
draft_version.id,
NULL,
'greeting',
'Saudacao inicial',
'Ola! Sou o Agente Virtual Sothis. Vou te direcionar para o atendimento correto de RH. Digite o numero da opcao que melhor descreve voce:\n\n1 - Sou colaborador ativo\n2 - Sou ex-colaborador\n3 - Sou candidato a uma vaga',
NULL,
'Nao consegui identificar seu perfil. Digite 1 para colaborador ativo, 2 para ex-colaborador ou 3 para candidato.',
2,
1,
0,
0,
CURRENT_TIMESTAMP
FROM draft_version
WHERE NOT EXISTS (
SELECT 1
FROM bot_flow_nodes node
WHERE node.version_id = draft_version.id
AND node.node_type = 'greeting'
)
RETURNING id, version_id
)
UPDATE bot_flow_versions version
SET root_node_id = inserted_root.id,
updated_at = CURRENT_TIMESTAMP
FROM inserted_root
WHERE version.id = inserted_root.version_id
AND version.root_node_id IS NULL;

View File

@ -0,0 +1,12 @@
-- ============================================================
-- Migration 020: No terminal de encerramento pelo bot
-- Tabelas:
-- bot_flow_nodes
-- ============================================================
ALTER TABLE bot_flow_nodes
DROP CONSTRAINT IF EXISTS bot_flow_nodes_node_type_check;
ALTER TABLE bot_flow_nodes
ADD CONSTRAINT bot_flow_nodes_node_type_check
CHECK (node_type IN ('greeting', 'question', 'agent', 'close'));

View File

@ -0,0 +1,41 @@
-- ============================================================
-- Migration 021: Auditoria e conteúdos da IA
-- Tabelas:
-- admin_audit_logs
-- ai_knowledge_contents
-- ============================================================
CREATE TABLE IF NOT EXISTS admin_audit_logs (
id SERIAL PRIMARY KEY,
actor_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
actor_name VARCHAR(180),
action VARCHAR(120) NOT NULL,
target_type VARCHAR(80),
target_id VARCHAR(120),
details TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at
ON admin_audit_logs (created_at DESC, id DESC);
CREATE TABLE IF NOT EXISTS ai_knowledge_contents (
id SERIAL PRIMARY KEY,
title VARCHAR(220) NOT NULL,
area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
filename VARCHAR(260),
mimetype VARCHAR(160),
file_size INTEGER,
content_base64 TEXT,
status VARCHAR(40) NOT NULL DEFAULT 'available',
notes TEXT,
created_by_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_contents_area
ON ai_knowledge_contents (area_id, status);
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_contents_created_at
ON ai_knowledge_contents (created_at DESC, id DESC);

View File

@ -0,0 +1,14 @@
-- ============================================================
-- Migration 022: Categoria de templates WhatsApp
-- Tabela: whatsapp_templates
-- ============================================================
ALTER TABLE whatsapp_templates
ADD COLUMN IF NOT EXISTS category VARCHAR(40) NOT NULL DEFAULT 'UTILITY';
UPDATE whatsapp_templates
SET category = 'UTILITY'
WHERE category IS NULL OR category = '';
CREATE INDEX IF NOT EXISTS idx_whatsapp_templates_category
ON whatsapp_templates (category);