const axios = require('axios') const flatten = require('flat') const CustomError = require('../errors') const CRM_Contact = require('../models/CRM_Contact') const CRM = require('../models/CRM') const { URL } = require('url') const { getAccessToken } = require('./oauth2') const findProperty = require('./findProperty') const requestConfigHeader = require('./requestConfigHeader') const sendMessageSocket = require('./sendMessageSocket') const CRM_Ticket = require('../models/CRM_Ticket') /** * @description Realiza a busca de um contato no CRM externo pelo e-mail. * Se o contato for encontrado, retorna suas informações. * Também lida com a lógica de cache e de tratamento de duplicatas (ex: Hubspot). * * @param {object} rest - Configurações REST do CRM. * @param {object} authentication - Detalhes de autenticação do CRM. * @param {string} crmEmail - O endereço de e-mail do contato a ser procurado. * @param {string} companyId - O ID da empresa. * @param {object} [test={}] - Objeto para controle de testes/mensagens de socket. * @param {boolean} [cacheContact=false] - Indica se deve verificar o cache local antes da API. * @returns {Promise} Um objeto com { exist: boolean, contactId: string, email: string, name: string, accountId: string } ou { exist: false }. */ async function lookupContactByEmail(rest, authentication, crmEmail, companyId, test = {}, cacheContact = false) { // Acessa a configuração específica para busca de contato por e-mail let { request, body, response } = findProperty(rest, 'lookupContactByEmail') if (!request || !response) { console.error('Configuração de "lookupContactByEmail" ausente ou incompleta no CRM REST properties.'); return { exist: false }; } let { requestContentType, requestEncoding, requestType, url } = request const { type, userName, passWord, token, crmClientId, crmAccountId } = authentication // 1. Tenta buscar o contato no cache local (CRM_Contact) pelo e-mail if (cacheContact) { const crmInfo = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, email: crmEmail }) if (crmInfo) { console.log(`[${new Date()}] Contato encontrado em cache por e-mail: ${crmEmail}`); return { exist: true, contactId: crmInfo.contactId, email: crmEmail } } } // 2. Prepara a configuração da requisição HTTP para a API do CRM // A 'requestConfigHeader' precisa ser capaz de lidar com a busca por e-mail const config = await requestConfigHeader(url, crmEmail, requestType, requestContentType, type, userName, passWord, token, crmClientId, '', '', companyId) if (test?.testing) { let msg = `Tentando checar se o contato de e-mail ${crmEmail} existe no crm` sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } }) } console.log("PAYLOAD CONFIG LOOKUP CONTACT BY EMAIL: ", JSON.stringify(config, null, 6)) let data try { let { data: _data } = await axios(config) data = _data } catch (error) { if (error.response) { console.error('==================> lookupContactByEmail Erro na resposta da API:', { status: error.response.status, data: error.response.data, }) } else if (error.request) { console.error('==================> lookupContactByEmail Nenhuma resposta recebida da API:', error.request) } else { console.error('==================> lookupContactByEmail Erro ao configurar a request:', error.message) } // throw error } console.log('CONTACT LOOKUP BY EMAIL DATA: ', JSON.stringify(data, null, 6)) // 3. Lógica específica para Hubspot (ou outros CRMs com tratamento especial de duplicatas) // Se o CRM for Hubspot e retornar múltiplos contatos, ele tentará encontrar o mais relevante. if (url.includes("hubapi") && data?.contacts && data.contacts.length > 1) { // Assume que 'hs_full_name_or_email' pode ser usado para encontrar o contato "principal" const auxContatWithName = data.contacts.find((c) => c.properties?.hs_full_name_or_email?.value?.trim().length > 2) if (auxContatWithName) { console.log(`[${new Date()}] ****** HUBSPOT CONTATO DUPLICADO POR EMAIL. CONTACTID ${auxContatWithName.vid} A SER CONSIDERADO ****** `) data.contacts = [auxContatWithName] // Considera apenas este contato para o fluxo // Lógica para limpar duplicatas no CRM_Contact local (se aplicável ao e-mail) const contacts = await CRM_Contact.find({ companyId, crmBaseURL: new URL(url).hostname, email: crmEmail }) if (contacts && contacts.length > 1) { for (const contact of contacts.slice(0, -1)) { // Deleta todos, exceto o último/mais relevante await CRM_Ticket.deleteMany({ companyId, contact: contact }) await contact.deleteOne() } } console.log(`[${new Date()}] dados do contato duplicado no crm (ajustado): `, data) const updateResult = await CRM_Contact.updateOne( { companyId, crmBaseURL: new URL(url).hostname, email: crmEmail }, // Atualiza pelo e-mail { $set: { contactId: auxContatWithName.vid } }) if (updateResult.modifiedCount > 0) return { exist: true, contactId: auxContatWithName.vid } } } // 4. Achata o objeto de resposta para facilitar a busca por propriedades data = flatten(data) let auxEmail // Variável para o e-mail encontrado na resposta let auxContactId let auxName let auxAccountId // 5. Itera sobre as propriedades do objeto 'data' para extrair e-mail e ID do contato // Baseado nas configurações de 'response' do seu CRM REST (response.email e response.id) for (const prop in data) { const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '') if (_prop == response?.email?.trim()) { // Procura pelo campo de e-mail na resposta auxEmail = data[prop] } if (_prop == response?.id?.trim()) { auxContactId = data[prop] } if (auxEmail && auxContactId) break // Se encontrou ambos, pode sair do loop } // Caso não encontre nas primeiras iterações (alguns CRMs podem ter estruturas aninhadas ou diferentes) // Esta parte é para casos onde o 'phone' e 'id' não são encontrados de primeira // (mantendo a lógica do seu lookupContactByPhone, mas adaptada para email) if (!auxEmail && !auxContactId) { // Se ainda não encontrou e-mail e ID for (const prop in data) { let _prop = prop.replace(/\.(\d+)(\.|$)/g, '[$1]$2') // SALESFORCE GETTING THE NAME if (_prop == response?.name?.trim()) { auxName = data[prop] } // SALESFORCE GETTING THE ACCOUNT ID if (_prop == response?.accountId?.trim()) { auxAccountId = data[prop] } if (_prop == response?.email?.trim()) { // Procura novamente pelo campo de e-mail auxEmail = data[prop] } if (_prop == response?.id?.trim()) { auxContactId = data[prop] } // SALESFORCE CASE LOOOK UP ALL THE OBJECT PROPERTIES // Se não for salesforce, pode sair do loop assim que encontrar email e ID if (!url.includes('salesforce')) if (auxEmail && auxContactId) break } } // Tenta pegar o accountId no body da resposta se não conseguir, pega do template // se a propriedade crmAccountId existir no template. Isso evita problemas com integrações // já em funcionamento. if (!auxAccountId && crmAccountId) { console.log('---------> auxAccountId definido a partir do crmAccountId do template: ', crmAccountId) auxAccountId = crmAccountId } console.log('---------> auxEmail: ', auxEmail, ' | auxContactId: ', auxContactId, ' | auxAccountId: ', auxAccountId) // 6. Se um e-mail válido for encontrado na resposta da API if (auxEmail) { if (auxEmail && auxContactId) { // Garante apenas 1 contato no seu banco local (CRM_Contact) const contacts = await CRM_Contact.find({ companyId, crmBaseURL: new URL(url).hostname, email: crmEmail, // Busca pelo e-mail original para evitar duplicatas locais contactId: { $ne: auxContactId } // O ID encontrado na API deve ser o único para este e-mail }) if (contacts && contacts.length > 0) { for (const contact of contacts) { console.log("=====> DELETING DUPLICATE CONTACTS BY EMAIL: ", contact) await CRM_Ticket.deleteMany({ companyId, contact: contact }) await contact.deleteOne() } } // Atualiza ou cria o contato no seu banco local (CRM_Contact) const contactInfo = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, contactId: auxContactId }) console.log('contactInfo (local): ', contactInfo, " | crmEmail: ", crmEmail) if (!contactInfo) { console.log('----------------> CREATE CONTACT MONGO (from email lookup)') const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname }) // await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, email: auxEmail }) // Armazena o e-mail } else if (contactInfo.email !== auxEmail) { // Se o contato existe mas o email mudou console.log('----------------> UPDATING CONTACT EMAIL IN MONGO') await CRM_Contact.updateOne( { companyId, crmBaseURL: new URL(url).hostname, contactId: auxContactId }, { $set: { email: auxEmail } } ); } } // Retorna que o contato existe e suas informações return { exist: true, contactId: auxContactId, email: crmEmail, name: auxName, accountId: auxAccountId } } // Se nenhum e-mail válido foi encontrado na resposta da API, o contato não existe return { exist: false } } module.exports = lookupContactByEmail