sgp - pedro afonso
This commit is contained in:
1 parent
ddf426fd5f
commit
828b6e7209
6 files changed
+3432
-2665
No files matched your search
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+127
-100
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user