sgp - pedro afonso

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-10-14 15:10:30 -03:00
1 parent ddf426fd5f
commit 828b6e7209
6 files changed
+3432 -2665

No files matched your search

+72 -60
View File
@@ -1,60 +1,72 @@
# Variáveis de ambiente
TELEGRAM_BOT_TOKEN=seu_token_do_bot_aqui
# Configurações SSH (exemplo)
DEFAULT_SSH_HOST=192.168.1.1
DEFAULT_SSH_USER=admin
DEFAULT_SSH_PASSWORD=password
# Configurações do bot
BOT_DEBUG=true
# IDs do Telegram com acesso a TODAS as cidades/provedores (admin)
ALLOWED_USERS=123456789,987654321
# ==========================================================
# Acesso por cidade/provedor (IDs do Telegram, separados por vírgula)
# Um usuário pode estar em mais de uma lista; se estiver em mais de
# uma cidade, o bot exibirá um menu para escolher qual atender.
# ==========================================================
GUARAI_ALLOWED_USERS=
PEDRO_AFONSO_ALLOWED_USERS=
# ==========================================================
# Concentrador Juniper - Guaraí
# ==========================================================
JUNIPER_GUARAI_HOST=131.0.192.254
JUNIPER_GUARAI_USER=rodrigo
JUNIPER_GUARAI_PASS=
# ==========================================================
# Concentrador Juniper - Pedro Afonso
# ==========================================================
JUNIPER_PEDRO_AFONSO_HOST=45.189.140.2
JUNIPER_PEDRO_AFONSO_USER=rodrigo
JUNIPER_PEDRO_AFONSO_PASS=
# Configurações TopSapp - Instância Guarai/Colmeia
TOPSAPP_GUARAI_USER=seu_usuario_guarai
TOPSAPP_GUARAI_PASS=sua_senha_guarai
# Configurações TopSapp - Instância Pedro Afonso
TOPSAPP_PEDRO_AFONSO_USER=seu_usuario_pedro_afonso
TOPSAPP_PEDRO_AFONSO_PASS=sua_senha_pedro_afonso
# ==========================================================
# Configurações GenieACS (TR-069 ACS)
# Porta padrão NBI: 7557
# ==========================================================
# Instância Guarai - Clicknet
GENIEACS_GUARAI_URL=http://g.clicknetto.com.br
GENIEACS_GUARAI_NBI_PORT=7557
GENIEACS_GUARAI_USER=api
GENIEACS_GUARAI_PASS=sua_senha_genieacs_guarai
# Exemplo de nova instância futura:
# GENIEACS_PEDRO_AFONSO_URL=http://genieacs.exemplo.com.br
# GENIEACS_PEDRO_AFONSO_NBI_PORT=7557
# GENIEACS_PEDRO_AFONSO_USER=api
# GENIEACS_PEDRO_AFONSO_PASS=sua_senha
# Variáveis de ambiente
TELEGRAM_BOT_TOKEN=seu_token_do_bot_aqui
# Configurações SSH (exemplo)
DEFAULT_SSH_HOST=192.168.1.1
DEFAULT_SSH_USER=admin
DEFAULT_SSH_PASSWORD=password
# Configurações do bot
BOT_DEBUG=true
# IDs do Telegram com acesso a TODAS as cidades/provedores (admin)
ALLOWED_USERS=123456789,987654321
# ==========================================================
# Acesso por cidade/provedor (IDs do Telegram, separados por vírgula)
# Um usuário pode estar em mais de uma lista; se estiver em mais de
# uma cidade, o bot exibirá um menu para escolher qual atender.
# ==========================================================
GUARAI_ALLOWED_USERS=
PEDRO_AFONSO_ALLOWED_USERS=
# ==========================================================
# Concentrador Juniper - Guaraí
# ==========================================================
JUNIPER_GUARAI_HOST=131.0.192.254
JUNIPER_GUARAI_USER=rodrigo
JUNIPER_GUARAI_PASS=
# ==========================================================
# Concentrador Juniper - Pedro Afonso
# ==========================================================
JUNIPER_PEDRO_AFONSO_HOST=45.189.140.2
JUNIPER_PEDRO_AFONSO_USER=rodrigo
JUNIPER_PEDRO_AFONSO_PASS=
# Configurações TopSapp - Instância Guarai/Colmeia
TOPSAPP_GUARAI_USER=seu_usuario_guarai
TOPSAPP_GUARAI_PASS=sua_senha_guarai
# Configurações SGP - Pedro Afonso (substitui o TopSapp nesta cidade)
# Autenticação: app + token (Sistema -> Ferramentas -> Painel Admin -> Tokens)
# ou, alternativamente, usuário + senha do SGP (Basic Auth)
SGP_PEDRO_AFONSO_URL=https://seu-sgp.exemplo.com.br
SGP_PEDRO_AFONSO_APP=nome_da_aplicacao
SGP_PEDRO_AFONSO_TOKEN=token_da_aplicacao
#SGP_PEDRO_AFONSO_USER=
#SGP_PEDRO_AFONSO_PASS=
# Opcional: id da OLT no SGP -> id_ap legado (2 = Pedro Afonso, 4 = Canavieiras).
# Se omitido, é deduzido pelo nome da OLT no SGP.
#SGP_PEDRO_AFONSO_OLT_MAP=12:2,15:4
# Opcional: ids de status de contrato do SGP (padrão: 1 = Ativo, 4 = Suspenso)
#SGP_STATUS_ATIVO=1
#SGP_STATUS_BLOQUEADO=4
#SGP_INSECURE_TLS=false
# ==========================================================
# Configurações GenieACS (TR-069 ACS)
# Porta padrão NBI: 7557
# ==========================================================
# Instância Guarai - Clicknet
GENIEACS_GUARAI_URL=http://g.clicknetto.com.br
GENIEACS_GUARAI_NBI_PORT=7557
GENIEACS_GUARAI_USER=api
GENIEACS_GUARAI_PASS=sua_senha_genieacs_guarai
# Exemplo de nova instância futura:
# GENIEACS_PEDRO_AFONSO_URL=http://genieacs.exemplo.com.br
# GENIEACS_PEDRO_AFONSO_NBI_PORT=7557
# GENIEACS_PEDRO_AFONSO_USER=api
# GENIEACS_PEDRO_AFONSO_PASS=sua_senha
+4
View File
@@ -6899,6 +6899,10 @@ Digite /help para ver todos os comandos disponíveis!
dadosSuporte.outrasInformacoes = `Criado via Bot Telegram\nData original: ${dados.dataHoraTexto}`;
// Usados pelo SGP (técnico responsável e agendamento são definidos na abertura do chamado)
dadosSuporte.idTecnico = dados.tecnicoSelecionado?.id;
dadosSuporte.dataHoraAgendamento = dados.dataHora;
const resultado = await this.topsapp.adicionarSuporte(dadosSuporte, location);
if (resultado.success) {
+718
View File
@@ -0,0 +1,718 @@
const axios = require('axios');
const https = require('https');
const logger = require('../utils/logger');
/**
* Cliente da API do SGP (https://documenter.getpostman.com/view/6682240/2sB34hHg2V).
*
* Expõe a mesma superfície que o TopSappAPI usa no TopSappManager e devolve os
* dados no mesmo formato ({ success, data: { dados: [...] }, instance }), de
* modo que o restante do bot não precise saber qual sistema está por trás.
*
* Convenções de mapeamento:
* - idCliente -> id do cliente no SGP
* - id_servico -> id do CONTRATO no SGP (bloqueio/liberação são feitos por contrato)
* - id da fatura -> id do título no SGP
*/
// Status de contrato no SGP (documentação pública)
const SGP_STATUS_ATIVO = parseInt(process.env.SGP_STATUS_ATIVO || '1', 10);
const SGP_STATUS_BLOQUEADO = parseInt(process.env.SGP_STATUS_BLOQUEADO || '4', 10); // Suspenso
class SgpAPI {
constructor() {
this.instances = {
pedro_afonso: {
key: 'pedro_afonso',
name: 'Pedro Afonso (SGP)',
cities: ['pedro afonso', 'pedro_afonso']
}
};
this.httpsAgent = new https.Agent({
rejectUnauthorized: process.env.SGP_INSECURE_TLS !== 'true'
});
this.tituloCache = new Map(); // id título -> { at, titulo }
this.tecnicoCache = new Map(); // id técnico -> username
console.log('🔗 SgpAPI inicializado');
}
// ------------------------------------------------------------------ infra
getInstanceByLocation() {
return this.instances.pedro_afonso;
}
getInstances() {
return Object.values(this.instances);
}
clearExpiredSessions() {
const now = Date.now();
for (const [id, entry] of this.tituloCache.entries()) {
if (now - entry.at > 2 * 60 * 1000) this.tituloCache.delete(id);
}
}
_name(credentials) {
return this.getInstanceByLocation().name;
}
/**
* Requisição genérica ao SGP. Autentica com token+app (corpo/query) ou Basic Auth.
*/
async _request(credentials, method, path, params = {}) {
if (!credentials || !credentials.baseURL) {
throw new Error('SGP não configurado (URL ausente)');
}
const clean = {};
Object.keys(params).forEach(k => {
if (params[k] !== undefined && params[k] !== null && params[k] !== '') clean[k] = params[k];
});
const config = {
method,
url: `${credentials.baseURL.replace(/\/$/, '')}${path}`,
httpsAgent: this.httpsAgent,
timeout: 30000
};
if (credentials.app && credentials.token) {
clean.app = credentials.app;
clean.token = credentials.token;
} else if (credentials.usuario && credentials.senha) {
config.auth = { username: credentials.usuario, password: credentials.senha };
} else {
throw new Error('SGP sem credenciais (app/token ou usuário/senha)');
}
if (method === 'GET') {
config.params = clean;
} else {
const body = new URLSearchParams();
Object.keys(clean).forEach(k => body.append(k, String(clean[k])));
config.data = body.toString();
config.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
const response = await axios(config);
return response.data;
}
/**
* Envolve uma chamada no formato { success, data, instance } / { success:false, error }
*/
async _wrap(credentials, fn) {
try {
const data = await fn();
return { success: true, data, instance: this._name(credentials) };
} catch (error) {
const body = error.response?.data;
const msg = (body && (body.msg || body.mensagem || body.detail || body.message || body.erro))
|| error.message;
logger.warn('Erro na API SGP', { error: msg, status: error.response?.status });
console.error(`❌ Erro SGP: ${msg}`);
return { success: false, error: typeof msg === 'string' ? msg : JSON.stringify(msg), instance: this._name(credentials) };
}
}
// --------------------------------------------------------------- helpers
_formatarData(data) {
if (!data) return '';
const m = String(data).match(/^(\d{4})-(\d{2})-(\d{2})/);
return m ? `${m[3]}/${m[2]}/${m[1]}` : String(data);
}
_statusId(texto) {
const t = String(texto || '').toLowerCase()
.normalize('NFD').replace(/[̀-ͯ]/g, '').trim();
if (/^ativo/.test(t)) return 1;
if (/suspens|bloquead/.test(t)) return 2;
if (/cancelad|inativ|desativ/.test(t)) return 3;
return 5; // aguardando / inviabilidade / outros
}
_textoStatus(idStatus) {
switch (idStatus) {
case 1: return 'liberado';
case 2: return 'bloqueado';
case 3: return 'desativado';
default: return 'desconhecido';
}
}
/** id_ap legado (2 = Pedro Afonso, 4 = Canavieiras) a partir da OLT do SGP */
_idApLegado(onu) {
if (!onu) return '';
const mapa = {};
(process.env.SGP_PEDRO_AFONSO_OLT_MAP || '').split(',').forEach(par => {
const [sgpId, legado] = par.split(':').map(s => s && s.trim());
if (sgpId && legado) mapa[sgpId] = legado;
});
if (onu.olt_id !== undefined && mapa[String(onu.olt_id)]) return mapa[String(onu.olt_id)];
const nome = String(onu.olt_nome || '').toLowerCase();
if (/canavieiras/.test(nome)) return '4';
if (/pedro|afonso/.test(nome)) return '2';
return onu.olt_id !== undefined ? String(onu.olt_id) : '';
}
_clienteId(c) {
return c.id ?? c.clienteId ?? c.cliente_id ?? c.idCliente ?? null;
}
_mapCliente(c) {
const contratos = c.contratos || [];
const ativo = contratos.some(ct => this._statusId(ct.status) === 1);
const contatos = c.contatos || c.telefones || [];
const primeiroContato = Array.isArray(contatos) && contatos.length
? (typeof contatos[0] === 'string' ? contatos[0] : (contatos[0].contato || contatos[0].numero || ''))
: '';
const end = c.endereco || {};
return {
id: this._clienteId(c),
nome: c.nome,
cpf: c.cpfcnpj || '',
cpfCnpj: c.cpfcnpj || '',
email: c.email || (Array.isArray(c.emails) ? c.emails[0] : '') || '',
telefone: c.telefone || c.celular || primeiroContato,
celular: c.celular || c.telefone || primeiroContato,
endereco: end.logradouro || '',
numero: end.numero || '',
bairro: end.bairro || '',
cidade: end.cidade || '',
uf: end.uf || '',
cep: end.cep || '',
complemento: end.complemento || '',
observacoes: c.observacao_cliente || '',
data_cadastro: c.dataCadastro || '',
status: ativo ? 1 : 0,
statusOriginal: contratos.map(ct => ct.status).join(', '),
id_status: ativo ? 1 : 0
};
}
_mapServicos(c) {
const idCliente = this._clienteId(c);
const end = c.endereco || {};
const out = [];
(c.contratos || []).forEach(ct => {
const idStatus = this._statusId(ct.status);
(ct.servicos && ct.servicos.length ? ct.servicos : [{}]).forEach(srv => {
const onu = srv.onu || {};
const conexao = onu.conexao || srv.conexao || {};
const e = srv.endereco || end;
out.push({
id: ct.contrato,
id_servico: ct.contrato,
servico_id: srv.id,
id_cliente: idCliente,
idCliente,
descricao: srv.plano?.descricao || srv.tipo || 'Serviço',
nome: srv.plano?.descricao || srv.tipo || 'Serviço',
plano_nome: srv.plano?.descricao || '',
tipo_servico: srv.tipo || '',
id_status: idStatus,
status: this._textoStatus(idStatus),
motivo_status: ct.motivo_status || '',
data_cadastro: ct.dataCadastro || '',
usuario: srv.login || onu.login || '',
login: srv.login || onu.login || '',
pppoe_login: srv.login || '',
senha: srv.senha || '',
ip: srv.ip || '',
mac: srv.mac || '',
// Dados do cliente (o TopSapp os devolvia embutidos no serviço)
instalacao_nome: c.nome,
cpf: c.cpfcnpj || '',
instalacao_endereco: e.logradouro || '',
instalacao_numero: e.numero || '',
instalacao_bairro: e.bairro || '',
instalacao_cidade: e.cidade || '',
instalacao_uf: e.uf || '',
instalacao_cep: e.cep || '',
instalacao_complemento: e.complemento || '',
latitude: e.latitude || '',
longitude: e.longitude || '',
// FTTH
tecnologia: 'FTTH',
base: onu.olt_nome || '',
id_ap: this._idApLegado(onu),
id_olt_sgp: onu.olt_id,
pon: onu.pon !== undefined ? `${onu.slot ?? ''}/${onu.pon}` : '',
perfil: '',
serial: onu.serial || '',
serviceport: '',
ultimo_rx_power: onu.rx || '',
ultimo_tx_power: onu.tx || '',
data_ultimo_sinal: '',
hora_ultimo_sinal: '',
conexao: conexao.status || '',
ultimo_acesso: conexao.data_conexao || '',
ultimo_ip: conexao.ip || '',
dhcp_username: '',
dhcp_agent_circuit_id: '',
dhcp_client_id: '',
dhcp_liberar: undefined
});
});
});
return out;
}
async _listarClientes(credentials, params) {
const todos = [];
let offset = 0;
for (let pagina = 0; pagina < 20; pagina++) {
const data = await this._request(credentials, 'POST', '/api/ura/clientes/', {
omitir_titulos: 1,
limit: 100,
offset,
...params
});
const lote = data.clientes || [];
todos.push(...lote);
const total = data.paginacao?.total ?? lote.length;
offset += lote.length;
if (lote.length === 0 || offset >= total) break;
}
return todos;
}
// --------------------------------------------------------------- clientes
async buscarCliente(query, location = '', credentials) {
const instance = this.getInstanceByLocation();
try {
const q = String(query || '').trim();
const params = {};
const digitos = q.replace(/\D/g, '');
if (/^\d+$/.test(q) && q.length !== 11 && q.length !== 14) {
params.cliente_id = parseInt(q, 10);
} else if (digitos.length === 11 || digitos.length === 14) {
params.cpfcnpj = digitos;
} else {
params.cliente_nome = q;
}
console.log(`🔍 [SGP] Buscando cliente:`, params);
const clientes = await this._listarClientes(credentials, params);
return {
success: true,
clientes: clientes.map(c => this._mapCliente(c)),
instance: instance.name
};
} catch (error) {
console.error('❌ [SGP] Erro na busca de cliente:', error.message);
return { success: false, error: error.response?.data?.msg || error.message, instance: instance.name };
}
}
async obterServicosCliente(idCliente, location = '', credentials) {
return this._wrap(credentials, async () => {
const id = parseInt(idCliente, 10);
if (isNaN(id)) throw new Error(`ID do cliente inválido: ${idCliente}`);
const clientes = await this._listarClientes(credentials, { cliente_id: id });
const dados = [];
clientes.forEach(c => dados.push(...this._mapServicos({ ...c, id: this._clienteId(c) ?? id })));
return { dados };
});
}
async obterDadosCompletosCliente(idCliente, location = '', credentials) {
const instance = this.getInstanceByLocation();
try {
const id = parseInt(idCliente, 10);
const clientes = await this._listarClientes(credentials, { cliente_id: id });
if (!clientes.length) {
return { success: false, error: 'Cliente não encontrado', instance: instance.name };
}
const c = { ...clientes[0], id: this._clienteId(clientes[0]) ?? id };
const cliente = this._mapCliente(c);
const primeiro = this._mapServicos(c)[0] || {};
cliente.latitude = primeiro.latitude || '';
cliente.longitude = primeiro.longitude || '';
return { success: true, cliente, instance: instance.name, fonte: 'sgp' };
} catch (error) {
return { success: false, error: error.response?.data?.msg || error.message, instance: instance.name };
}
}
async obterDadosCliente(idCliente, location = '', credentials) {
const r = await this.obterDadosCompletosCliente(idCliente, location, credentials);
if (!r.success) return r;
return { success: true, data: { clientes: [r.cliente] }, instance: r.instance };
}
interpretarStatusCliente(cliente) {
return cliente.status === 1 || cliente.id_status === 1 ? 1 : 0;
}
interpretarStatusServico(servico) {
return servico.id_status || this._statusId(servico.status);
}
obterTextoStatus(idStatus) {
return this._textoStatus(parseInt(idStatus, 10));
}
// ----------------------------------------------------------------- faturas
_mapTitulo(t, idCliente = null) {
const pago = t.status === 'pago' || !!t.dataPagamento;
return {
id: t.id,
valor: t.valorCorrigido ?? t.valor,
valor_original: t.valor,
vencimento: this._formatarData(t.dataVencimento),
pago: pago ? 'SIM' : 'NAO',
historico: t.demonstrativo || `Fatura ${t.numeroDocumento || t.id}`,
descricao: t.demonstrativo || 'Mensalidade',
periodo_inicio: '',
periodo_fim: '',
codigo_cliente: idCliente,
id_servico: t.clienteContrato,
numero_documento: t.numeroDocumento,
link: t.link,
linha_digitavel: t.linhaDigitavel || '',
codigo_barras: t.codigoBarras || '',
qrcode: t.codigoPix || ''
};
}
async obterFaturas(idCliente, location = '', credentials, idServico = null) {
return this._wrap(credentials, async () => {
const params = { status: 'abertos' };
if (idCliente) params.cliente_id = parseInt(idCliente, 10);
if (idServico) params.contrato = parseInt(idServico, 10);
const dados = [];
let offset = 0;
for (let pagina = 0; pagina < 20; pagina++) {
const data = await this._request(credentials, 'POST', '/api/ura/titulos/', {
...params, limit: 250, offset
});
const lote = data.titulos || [];
lote.forEach(t => {
this.tituloCache.set(String(t.id), { at: Date.now(), titulo: t });
dados.push(this._mapTitulo(t, idCliente));
});
const total = data.paginacao?.total ?? lote.length;
offset += lote.length;
if (lote.length === 0 || offset >= total) break;
}
return { dados };
});
}
async _obterTitulo(idFatura, credentials) {
const cached = this.tituloCache.get(String(idFatura));
if (cached && Date.now() - cached.at < 2 * 60 * 1000) return cached.titulo;
const data = await this._request(credentials, 'POST', '/api/ura/titulos/', {
titulo_id: parseInt(idFatura, 10)
});
const titulo = (data.titulos || [])[0];
if (!titulo) throw new Error('Fatura não encontrada');
this.tituloCache.set(String(idFatura), { at: Date.now(), titulo });
return titulo;
}
async obterDadosFatura(idFatura, idCliente, location = '', credentials) {
return this._wrap(credentials, async () => {
const titulo = await this._obterTitulo(idFatura, credentials);
const f = this._mapTitulo(titulo, idCliente);
return { fatura: f };
});
}
/**
* Baixa o boleto em PDF e devolve em Base64 (mesmo contrato do TopSapp).
*/
async obterBoletoBase64(idFatura, location = '', credentials) {
return this._wrap(credentials, async () => {
const titulo = await this._obterTitulo(idFatura, credentials);
let link = null;
try {
const seg = await this._request(credentials, 'POST', '/api/ura/fatura2via/', {
contrato: titulo.clienteContrato,
numero_documento: titulo.numeroDocumento,
link_pdf: 1,
nao_gerar_os: 1
});
link = (seg.links || []).find(l => String(l.fatura) === String(idFatura))?.link || seg.link;
} catch (error) {
logger.warn('SGP fatura2via falhou, usando link do título', { error: error.message });
}
link = link || titulo.link;
if (!link) throw new Error('Boleto sem link disponível');
const resp = await axios.get(link, {
responseType: 'arraybuffer',
httpsAgent: this.httpsAgent,
timeout: 30000
});
const tipo = String(resp.headers['content-type'] || '');
if (!/pdf/i.test(tipo)) throw new Error(`O link do boleto não retornou PDF (${tipo || 'desconhecido'}): ${link}`);
return { arquivo: Buffer.from(resp.data).toString('base64'), link };
});
}
convertBase64ToPDF(base64String, filename = 'boleto.pdf') {
try {
const base64Data = base64String.replace(/^data:application\/pdf;base64,/, '');
return { success: true, buffer: Buffer.from(base64Data, 'base64'), filename, mimeType: 'application/pdf' };
} catch (error) {
return { success: false, error: error.message };
}
}
// ------------------------------------------------------ bloqueio/liberação
async _alterarStatusContrato(idContrato, status, credentials) {
return this._wrap(credentials, () => this._request(credentials, 'POST', '/api/ura/contrato/status/edit/', {
contrato: parseInt(idContrato, 10),
status
}));
}
async alterarStatusServico(idServico, idCliente, status, location = '', credentials) {
// status no formato TopSapp: 1 = ativo, 2 = bloqueado
return this._alterarStatusContrato(idServico, status === 1 ? SGP_STATUS_ATIVO : SGP_STATUS_BLOQUEADO, credentials);
}
async bloquearServico(idServico, idCliente, location = '', credentials) {
return this._alterarStatusContrato(idServico, SGP_STATUS_BLOQUEADO, credentials);
}
async liberarServico(idServico, idCliente, location = '', credentials) {
return this._alterarStatusContrato(idServico, SGP_STATUS_ATIVO, credentials);
}
// ----------------------------------------------------------------- suporte
async obterTecnicos(location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'POST', '/api/ura/tecnicos/');
(lista || []).forEach(t => this.tecnicoCache.set(String(t.id), t.username));
return { dados: (lista || []).map(t => ({ id: t.id, nome: t.nome, username: t.username })) };
});
}
/** Equivalente aos "operadores" do TopSapp: o bot só usa id_tecnico e tecnico */
async obterOperadores(location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'POST', '/api/ura/tecnicos/');
(lista || []).forEach(t => this.tecnicoCache.set(String(t.id), t.username));
return { dados: (lista || []).map(t => ({ id: t.id, id_tecnico: t.id, tecnico: t.nome, username: t.username })) };
});
}
async obterFuncionarios(location = '', credentials) {
return this.obterTecnicos(location, credentials);
}
async obterSuporteCategorias(location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'GET', '/api/os/ocorrencia/tipo/list/');
// "ocorrenciatipo" do chamado usa o código do tipo
return { dados: (lista || []).map(t => ({ id: t.codigo ?? t.id, descricao: t.descricao })) };
});
}
async obterSuporteTiposAtendimento(location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'GET', '/api/ura/ocorrencia/metodo/list/');
return { dados: (lista || []).map(m => ({ id: m.codigo, descricao: m.descricao })) };
});
}
/**
* Abre chamado (ocorrência + O.S.). idServico = contrato.
*/
async adicionarSuporte(dadosSuporte, location = '', credentials) {
return this._wrap(credentials, async () => {
const params = {
contrato: parseInt(dadosSuporte.idServico, 10),
conteudo: dadosSuporte.descricao,
conteudolimpo: 1,
ocorrenciatipo: dadosSuporte.idCategoria,
observacao: dadosSuporte.outrasInformacoes,
contato_nome: dadosSuporte.contato,
contato_telefone: dadosSuporte.contatoTelefone
};
if (dadosSuporte.idTecnico) {
const username = this.tecnicoCache.get(String(dadosSuporte.idTecnico));
if (username) params.responsavel = username;
}
if (dadosSuporte.dataHoraAgendamento) {
const d = new Date(dadosSuporte.dataHoraAgendamento);
if (!isNaN(d.getTime())) {
const p = n => String(n).padStart(2, '0');
params.data_hora_agendamento = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
}
const data = await this._request(credentials, 'POST', '/api/ura/chamado/', params);
if (data.status !== undefined && Number(data.status) !== 1) {
throw new Error(data.msg || 'Não foi possível abrir o chamado');
}
return { protocolo: data.protocolo, ...data };
});
}
/**
* O técnico já é definido na abertura do chamado (campo "responsavel").
*/
async encaminharSuporte(idSuporte, idTecnico, descricao, location = '', credentials) {
return { success: true, data: { msg: 'Técnico definido na abertura do chamado' }, instance: this._name(credentials) };
}
async obterSuportesAbertos(idCliente, location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'POST', '/api/os/list/', {
cliente_id: parseInt(idCliente, 10)
});
const dados = (lista || [])
.filter(os => Number(os.os_status) !== 1 && !os.os_data_finalizacao)
.map(os => ({
id: os.os_id,
protocolo: os.os_protocolo,
defeito: os.os_conteudo || os.os_motivo_descricao || '',
endereco: os.endereco_logradouro || '',
numero: os.endereco_numero || '',
bairro: os.endereco_bairro || '',
cidade: os.endereco_cidade || '',
data: os.os_data_agendamento || os.os_data_cadastro || ''
}));
return { dados };
});
}
async finalizarSuporte(idSuporte, descricao, location = '', credentials) {
return this._wrap(credentials, () => {
const d = new Date();
const p = n => String(n).padStart(2, '0');
const agora = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
return this._request(credentials, 'POST', `/api/os/update/id/${parseInt(idSuporte, 10)}/`, {
os_status: 1,
os_servicoprestado: descricao,
os_data_finalizacao: agora
});
});
}
// -------------------------------------------------------------------- FTTH
/**
* Lista OLTs no formato que o fluxo de vínculo de ONU espera (nome/ssid/portas/perfis).
* O SGP não exige porta/perfil para vincular uma ONU já autorizada, então
* são devolvidos valores neutros apenas para o fluxo seguir.
*/
async obterOlts(location = '', credentials) {
return this._wrap(credentials, async () => {
const lista = await this._request(credentials, 'GET', '/api/fttx/olt/list/');
return {
dados: (lista || []).map(o => ({
id: o.id,
nome: o.name,
ssid: o.host || '',
portas: [{ id: 0, alias: 'SGP' }],
perfis: [{ id: 0, identificacao: 'SGP' }]
}))
};
});
}
/**
* Vincula uma ONU a um serviço: localiza a ONU pelo serial no SGP e associa ao login do contrato.
*/
async atualizarFTTH(idCliente, idServico, ftthData, location = '', credentials) {
return this._wrap(credentials, async () => {
const serial = String(ftthData.serial || '').toUpperCase();
if (!serial) throw new Error('Serial da ONU não informado');
const servicos = await this.obterServicosCliente(idCliente, location, credentials);
if (!servicos.success) throw new Error(servicos.error);
const srv = servicos.data.dados.find(s => String(s.id_servico) === String(idServico));
if (!srv) throw new Error(`Contrato #${idServico} não encontrado para o cliente`);
if (!srv.login) throw new Error('Contrato sem login de serviço para vincular a ONU');
const onus = await this._request(credentials, 'GET', '/api/fttx/onu/list/');
const onu = (onus || []).find(o => String(o.phy_addr || '').toUpperCase() === serial);
if (!onu) {
throw new Error(`ONU ${serial} não está cadastrada/autorizada no SGP. Autorize-a no SGP e tente novamente.`);
}
return this._request(credentials, 'POST', `/api/fttx/onu/${onu.id}/edit/`, {
onu_update: 'service',
service: srv.login
});
});
}
// ------------------------------------------------------------------ RADIUS
/**
* Sessões PPPoE (equivalente a ObterClientesServicosAcessos).
* params: { status: 'on' } ou { dataInicial, dataFinal } (DD/MM/AAAA)
*/
async obterAcessos(params, instance, credentials) {
return this._wrap(credentials, async () => {
const query = { limit: 1000 };
if (params.status === 'on') query.online = 1;
const iso = d => {
const m = String(d || '').match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
return m ? `${m[3]}-${m[2]}-${m[1]}` : null;
};
const ini = iso(params.dataInicial);
const fim = iso(params.dataFinal);
const sessoes = [];
let offset = 0;
for (let pagina = 0; pagina < 20; pagina++) {
const data = await this._request(credentials, 'POST', '/ws/radius/radacct/list/all/', { ...query, offset });
const lote = data.result || [];
lote.forEach(item => {
(item.radacct || []).forEach(r => {
const inicio = String(r.acctstarttime || '');
if (ini && fim && inicio) {
const dia = inicio.slice(0, 10);
const termDia = r.acctstoptime ? String(r.acctstoptime).slice(0, 10) : '9999-12-31';
if (dia > fim || termDia < ini) return;
}
sessoes.push({
id_cliente: -1,
usuario: r.username || item.pppoe_login,
ip: r.framedipaddress || item.ip || '',
inicio,
termino: r.acctstoptime || '',
nas_ip: r.nasipaddress || '',
nome: item.nome
});
});
});
const total = data.paggination?.total ?? lote.length;
offset += lote.length;
if (lote.length === 0 || offset >= total) break;
}
return { dados: sessoes };
});
}
}
module.exports = SgpAPI;
+1063 -1073
View File
File diff suppressed because it is too large. Load diff
+127 -100
View File
@@ -1,100 +1,127 @@
/**
* Configurações do TopSapp
*/
class TopSappConfig {
constructor() {
this.credentials = {
guarai_colmeia: {
usuario: process.env.TOPSAPP_GUARAI_USER || '',
senha: process.env.TOPSAPP_GUARAI_PASS || '',
identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram',
idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1'
},
pedro_afonso: {
usuario: process.env.TOPSAPP_PEDRO_AFONSO_USER || '',
senha: process.env.TOPSAPP_PEDRO_AFONSO_PASS || '',
identificador: process.env.TOPSAPP_PEDRO_AFONSO_ID || 'BotTelegram',
idUsuario: process.env.TOPSAPP_PEDRO_AFONSO_USERID || '1'
}
};
this.isConfigured = this.validateConfiguration();
}
/**
* Validar se as configurações estão completas
*/
validateConfiguration() {
const required = ['usuario', 'senha'];
for (const instance in this.credentials) {
const creds = this.credentials[instance];
for (const field of required) {
if (!creds[field]) {
console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`);
return false;
}
}
}
console.log('✅ TopSapp: Configurações validadas');
return true;
}
/**
* Obter credenciais para uma instância
*/
getCredentials(instanceKey) {
return this.credentials[instanceKey] || null;
}
/**
* Obter credenciais a partir do site ativo (cidade/provedor)
*/
getCredentialsBySite(siteId) {
const sites = require('../config/sites');
const site = sites.getSite(siteId);
if (!site) return this.credentials.guarai_colmeia;
return this.credentials[site.topsappKey] || null;
}
/**
* Obter credenciais por localização
*/
getCredentialsByLocation(location = '') {
const loc = location.toLowerCase();
if (loc.includes('pedro') || loc.includes('afonso')) {
return this.credentials.pedro_afonso;
}
return this.credentials.guarai_colmeia;
}
/**
* Verificar se o TopSapp está configurado
*/
isReady() {
return this.isConfigured;
}
/**
* Obter status da configuração
*/
getStatus() {
const status = {};
for (const instance in this.credentials) {
const creds = this.credentials[instance];
status[instance] = {
configured: !!(creds.usuario && creds.senha),
usuario: creds.usuario ? '✅' : '❌',
senha: creds.senha ? '✅' : '❌'
};
}
return status;
}
}
module.exports = TopSappConfig;
/**
* Configurações do TopSapp
*/
class TopSappConfig {
constructor() {
this.credentials = {
guarai_colmeia: {
usuario: process.env.TOPSAPP_GUARAI_USER || '',
senha: process.env.TOPSAPP_GUARAI_PASS || '',
identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram',
idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1'
},
};
// Pedro Afonso migrou do TopSapp para o SGP
this.sgpCredentials = {
pedro_afonso: {
baseURL: process.env.SGP_PEDRO_AFONSO_URL || '',
app: process.env.SGP_PEDRO_AFONSO_APP || '',
token: process.env.SGP_PEDRO_AFONSO_TOKEN || '',
usuario: process.env.SGP_PEDRO_AFONSO_USER || '',
senha: process.env.SGP_PEDRO_AFONSO_PASS || ''
}
};
this.isConfigured = this.validateConfiguration();
}
/**
* Validar se as configurações estão completas
*/
validateConfiguration() {
const required = ['usuario', 'senha'];
for (const instance in this.credentials) {
const creds = this.credentials[instance];
for (const field of required) {
if (!creds[field]) {
console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`);
return false;
}
}
}
console.log('✅ TopSapp: Configurações validadas');
return true;
}
/**
* SGP configurado quando há URL e (app+token ou usuário+senha)
*/
isSgpConfigured(key = 'pedro_afonso') {
const c = this.sgpCredentials[key];
return !!(c && c.baseURL && ((c.app && c.token) || (c.usuario && c.senha)));
}
/**
* Obter credenciais para uma instância
*/
getCredentials(instanceKey) {
return this.credentials[instanceKey] || null;
}
/**
* Obter credenciais a partir do site ativo (cidade/provedor)
*/
getCredentialsBySite(siteId) {
const sites = require('../config/sites');
const site = sites.getSite(siteId);
if (!site) return this.credentials.guarai_colmeia;
return this.credentials[site.topsappKey] || null;
}
/**
* Obter credenciais por localização
*/
getCredentialsByLocation(location = '') {
if (this.usesSgp(location)) {
return this.sgpCredentials.pedro_afonso;
}
return this.credentials.guarai_colmeia;
}
/**
* Pedro Afonso é atendido pelo SGP; as demais localidades pelo TopSapp
*/
usesSgp(location = '') {
const loc = String(location || '').toLowerCase();
return loc.includes('pedro') || loc.includes('afonso');
}
/**
* Verificar se o TopSapp está configurado
*/
isReady(location) {
if (location === undefined) {
return this.isConfigured || this.isSgpConfigured();
}
return this.usesSgp(location) ? this.isSgpConfigured() : this.isConfigured;
}
/**
* Obter status da configuração
*/
getStatus() {
const status = {};
for (const instance in this.credentials) {
const creds = this.credentials[instance];
status[instance] = {
configured: !!(creds.usuario && creds.senha),
usuario: creds.usuario ? '✅' : '❌',
senha: creds.senha ? '✅' : '❌'
};
}
status.pedro_afonso_sgp = {
configured: this.isSgpConfigured(),
usuario: this.sgpCredentials.pedro_afonso.baseURL ? '✅' : '❌',
senha: (this.sgpCredentials.pedro_afonso.token || this.sgpCredentials.pedro_afonso.senha) ? '✅' : '❌'
};
return status;
}
}
module.exports = TopSappConfig;
+1448 -1432
View File
File diff suppressed because it is too large. Load diff