diff --git a/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_contact_14288.json b/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_contact_14288.json index e151de0..1832b07 100644 --- a/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_contact_14288.json +++ b/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_contact_14288.json @@ -68,6 +68,21 @@ } } }, + { + "lookupContactByEmail": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Get", + "responseType": "Json", + "url": "https://berlitz--blzlatam.sandbox.my.salesforce.com/services/data/v61.0/query/?q=SELECT+Id,Email+FROM+Contact+WHERE+Email='crmEmail'" + }, + "response": { + "email": "records[0].Email", + "id": "records[0].Id" + } + } + }, { "callJournaling": { "request": { diff --git a/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_lead_14288.json b/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_lead_14288.json index 28887d8..8b34960 100644 --- a/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_lead_14288.json +++ b/backend/Templates-test/TEMPLATES-DEFAULT/salesforce_berlitz_sandbox_lead_14288.json @@ -49,7 +49,7 @@ "LeadSource": "WhatsApp", "Country_Segmentation__c": "Brazil", "CurrencyIsoCode": "BRL", - "RecordTypeId": "0120b000000OhSqAAK" + "RecordTypeId": "0120b000000OhSsAAK" }, "response": { "id": "id" @@ -71,6 +71,21 @@ } } }, + { + "lookupContactByEmail": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Get", + "responseType": "Json", + "url": "https://berlitz--blzlatam.sandbox.my.salesforce.com/services/data/v61.0/query/?q=SELECT+Id,Email+FROM+Lead+WHERE+Email='crmEmail'" + }, + "response": { + "email": "records[0].Email", + "id": "records[0].Id" + } + } + }, { "callJournaling": { "request": { diff --git a/backend/controllers/crmController.js b/backend/controllers/crmController.js index dd895f4..fcfeab2 100644 --- a/backend/controllers/crmController.js +++ b/backend/controllers/crmController.js @@ -31,57 +31,118 @@ const redirectContactLinkCRM = require('../utils/redirectContactLinkCRM') const sfcase = require('../utils/sfCase') const sfCaseUpdate = require('../utils/sfCaseUpdate') const removeZeroInicial = require('../utils/removeZeroInicial') -const getSalesforceUser = require('../utils/getSalesforceUser') +const lookupContactByEmail = require('../utils/lookupCRMContactByEmail') const contactCreate = async (req, res) => { const { companyId, crmFirstName, crmLastName, crmPhone, crmEmail, dynamicBodyRequest } = req.body - mustContainProperties(req, ['companyId', 'crmPhone',]) - + // mustContainProperties(req, ['companyId', 'crmPhone',]) + + if (!companyId || (!crmPhone && !crmEmail)) { + return res.status(StatusCodes.BAD_REQUEST).json({ + message: 'companyId e crmPhone ou crmEmail são obrigatórios.' + }); + } await createCRMContact(companyId, crmFirstName, crmPhone, crmEmail, crmLastName, dynamicBodyRequest) res.status(StatusCodes.OK).send() } +// const checkContact = async (req, res) => { + +// const { companyId, crmPhone } = req.body + +// mustContainProperties(req, ['companyId', 'crmPhone',]) + +// const crmFiles = await loadCRM(companyId) + +// if (crmFiles.length > 0) { +// const { crmRest: rest, authentication } = crmFiles[0].crm + +// const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) + +// if (contact && contact.exist) { +// return res.status(StatusCodes.OK).json({ exist: contact.exist }) +// } +// } + +// res.status(StatusCodes.OK).json({ exist: false }) +// } const checkContact = async (req, res) => { + const { companyId, crmPhone, crmEmail } = req.body; - const { companyId, crmPhone } = req.body + if (!companyId || (!crmPhone && !crmEmail)) { + return res.status(StatusCodes.BAD_REQUEST).json({ + message: 'companyId and either crmPhone or crmEmail are required.' + }); + } - mustContainProperties(req, ['companyId', 'crmPhone',]) - - const crmFiles = await loadCRM(companyId) + const crmFiles = await loadCRM(companyId); if (crmFiles.length > 0) { - const { crmRest: rest, authentication } = crmFiles[0].crm + const { crmRest: rest, authentication } = crmFiles[0].crm; + let contact = null; - const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) - - if (contact && contact.exist) { - return res.status(StatusCodes.OK).json({ exist: contact.exist }) + if (crmPhone) { + contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId); + } + if (!contact?.exist && crmEmail) { + contact = await lookupContactByEmail(rest, authentication, crmEmail, companyId); + } + if (contact?.exist) { + return res.status(StatusCodes.OK).json({ exist: contact.exist }); } } - res.status(StatusCodes.OK).json({ exist: false }) -} + res.status(StatusCodes.OK).json({ exist: false }); +}; -const contactActivity = async (req, res) => { - const { companyId, crmPhone, ticketId } = req.body +// const contactActivity = async (req, res) => { +// const { companyId, crmPhone, ticketId } = req.body - mustContainProperties(req, ['companyId', 'crmPhone', 'ticketId']) +// mustContainProperties(req, ['companyId', 'crmPhone', 'ticketId']) +// await whatsappJournalingCRM( +// companyId, +// crmPhone, +// '0000', +// crmFirstName = "unnamed", +// ticketId +// ) + +// res.status(StatusCodes.OK).send() +// } +const contactActivity = async (req, res) => { + // 1. Recebe 'crmEmail' do corpo da requisição + const { companyId, crmPhone, crmEmail, ticketId, dynamicBodyRequest } = req.body; + + // 2. Ajusta a validação para que 'crmPhone' ou 'crmEmail' seja obrigatório + // Se nenhum dos dois for fornecido, a requisição é inválida. + if (!crmPhone && !crmEmail) { + return res.status(StatusCodes.BAD_REQUEST).send({ + message: "crmPhone ou crmEmail é obrigatório." + }); + } + + // A validação para 'companyId' e 'ticketId' continua, se forem obrigatórios + // mustContainProperties(req, ['companyId', 'ticketId']); + + // 3. Chama a função whatsappJournalingCRM, passando o crmEmail como o último parâmetro await whatsappJournalingCRM( companyId, crmPhone, '0000', crmFirstName = "unnamed", - ticketId - ) + ticketId, + crmEmail, + dynamicBodyRequest + ); - res.status(StatusCodes.OK).send() -} + res.status(StatusCodes.OK).send(); +}; const deleteCrm = async (req, res) => { @@ -258,9 +319,7 @@ const oauthCallBack = async (req, res) => { console.log('xxxxxxxxxx companyId: ', companyId) console.log('xxxxxxxxxx code: ', code) - if (code) { - - console.log('xxxxxxxxxx passed') + if (code) { let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, 'companyId': companyId }) diff --git a/backend/utils/createCRMContact.js b/backend/utils/createCRMContact.js index d0b8ff1..816f47d 100644 --- a/backend/utils/createCRMContact.js +++ b/backend/utils/createCRMContact.js @@ -1,18 +1,29 @@ const lookupContactByPhone = require('./lookupCRMContactByPhone') const createContact = require('./createContact') const loadCRM = require('./loadCRM') +const lookupContactByEmail = require('./lookupCRMContactByEmail') -async function createCRMContact(companyId, crmFirstName, crmPhone, crmEmail = '', crmLastName = '', dynamicBodyRequest = {}) { +async function createCRMContact(companyId, crmFirstName, crmPhone = '', crmEmail = '', crmLastName = '', dynamicBodyRequest = {}) { const crmFiles = await loadCRM(companyId) for (const crmConfig of crmFiles) { const { crmRest: rest, authentication } = crmConfig.crm + let contact = null - const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) + if (crmPhone) { + contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) + } + if (!contact?.exist && crmEmail) { + contact = await lookupContactByEmail(rest, authentication, crmEmail, companyId) + } + if (contact?.exist) { + continue + } + // const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) - if (contact.exist) continue + // if (contact.exist) continue await createContact(companyId, rest, authentication, crmPhone, crmFirstName, crmLastName, crmEmail, {}, dynamicBodyRequest) diff --git a/backend/utils/createContact.js b/backend/utils/createContact.js index 3589355..ccf34f4 100644 --- a/backend/utils/createContact.js +++ b/backend/utils/createContact.js @@ -8,11 +8,14 @@ const CRM = require('../models/CRM') const requestConfigHeader = require('./requestConfigHeader') const sendMessageSocket = require('./sendMessageSocket') -async function createContact(companyId, rest, authentication, crmPhone, crmFirstName = 'Username', crmLastName = 'Last name', crmEmail = '', test = {}, dynamicBodyRequest = {}) { +async function createContact(companyId, rest, authentication, crmPhone = '', crmFirstName = 'Username', crmLastName = 'Last name', crmEmail = '', test = {}, dynamicBodyRequest = {}) { let { request, body, response } = findProperty(rest, 'createContactRecord') const { requestContentType, requestEncoding, requestType, responseType, url } = request + // O identificador a ser usado na requisição (prioriza telefone, mas usa e-mail se não houver) + const lookupValue = crmPhone || crmEmail; + body = flatten(body) const mapping = { @@ -42,10 +45,12 @@ async function createContact(companyId, rest, authentication, crmPhone, crmFirst const { type, userName, passWord, token, crmClientId } = authentication //url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '', ticketId = '', companyId - let config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body, '', companyId) + // let config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body, '', companyId) + let config = await requestConfigHeader(url, lookupValue, requestType, requestContentType, type, userName, passWord, token, crmClientId, body, '', companyId) if (test?.testing) { - msg = `Tentanto criar contato de numero ${crmPhone} no crm` + // msg = `Tentanto criar contato de numero ${crmPhone} no crm` + msg = `Tentando criar contato com identificador ${lookupValue} no crm` sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } }) } @@ -98,9 +103,11 @@ async function createContact(companyId, rest, authentication, crmPhone, crmFirst } - if (auxContactId && !test?.testing) { + if (auxContactId && !test?.testing && crmEmail=='') { + const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname }) - await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: crmPhone }) + // await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: crmPhone }) + await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: crmPhone }) } return { exist: true, contactId: auxContactId, phone: crmPhone } diff --git a/backend/utils/extractLeadStatusChange.js b/backend/utils/extractLeadStatusChange.js new file mode 100644 index 0000000..383a309 --- /dev/null +++ b/backend/utils/extractLeadStatusChange.js @@ -0,0 +1,26 @@ +/** + * Extrai e valida informações de mudança de status do Lead a partir do dynamicBodyRequest. + * - newStatus: deve ser string não vazia + * - leadId: opcional; se ausente, pode ser fornecido externamente (ex.: contactId) + */ +function extractLeadStatusChange(dynamicBodyRequest = {}) { + if (!dynamicBodyRequest || typeof dynamicBodyRequest !== 'object') { + return { shouldUpdate: false } + } + + const rawStatus = dynamicBodyRequest.__newLeadStatus + const rawLeadId = dynamicBodyRequest.__leadId + + const newStatus = typeof rawStatus === 'string' ? rawStatus.trim() : '' + const leadId = typeof rawLeadId === 'string' ? rawLeadId.trim() : undefined + + if (!newStatus) { + return { shouldUpdate: false } + } + + return { shouldUpdate: true, newStatus, leadId } +} + +module.exports = extractLeadStatusChange + + diff --git a/backend/utils/journalingRequest.js b/backend/utils/journalingRequest.js index 5c9e579..31ed393 100644 --- a/backend/utils/journalingRequest.js +++ b/backend/utils/journalingRequest.js @@ -7,7 +7,7 @@ const path = require('path') const convertToIntegerIfNumber = require('./convertToIntegerIfNumber') const sendMessageSocket = require('./sendMessageSocket') // request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, rest, companyId -async function journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, test = {}, companyId='') { +async function journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, test = {}, companyId = '', dynamicBodyRequest = {}) { const { requestContentType, requestEncoding, requestType, responseType, url } = request console.log('----------> crmCallDuration: ', crmCallDuration) @@ -121,6 +121,21 @@ async function journalingRequest(request, body, crmCallDuration, contact, crmAge // url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '', ticketId = '', companyId const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data, '', companyId) + if (dynamicBodyRequest && Object.keys(dynamicBodyRequest).length !== 0) { + // Evita enviar chaves internas (metadados) para o Salesforce + const sanitized = Object.keys(dynamicBodyRequest).reduce((acc, key) => { + if (!key.startsWith('__')) acc[key] = dynamicBodyRequest[key] + return acc + }, {}) + + if (Object.keys(sanitized).length > 0) { + config.data = { ...config.data, ...sanitized } + } + console.log('#####################') + console.log('JOURNALING PAYLOAD UPDATED BY DYNAMIC BODY REQUEST: ', JSON.stringify(config, null, 6)) + console.log('#####################') + } + if (test?.testing && test?.companyId && test?.msg) { sendMessageSocket({ companyId: test.companyId, status: 'processing', data: { request: config, msg: test.msg } }) } diff --git a/backend/utils/lookupCRMContactByEmail.js b/backend/utils/lookupCRMContactByEmail.js new file mode 100644 index 0000000..01b5ddd --- /dev/null +++ b/backend/utils/lookupCRMContactByEmail.js @@ -0,0 +1,234 @@ +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 + + diff --git a/backend/utils/requestConfigHeader.js b/backend/utils/requestConfigHeader.js index 50a7107..6a023c2 100644 --- a/backend/utils/requestConfigHeader.js +++ b/backend/utils/requestConfigHeader.js @@ -1,10 +1,16 @@ const { getAccessToken } = require('./oauth2') -async function requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '', ticketId = '', companyId) { +async function requestConfigHeader(url, lookupValue, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '', ticketId = '', companyId) { let config = {} console.log('requestConfigHeader ticketId: ', ticketId) - url = url.replace('crmPhone', crmPhone) + // url = url.replace('crmPhone', crmPhone) + if (url.includes('crmPhone')) { + url = url.replace('crmPhone', lookupValue) + } else if (url.includes('crmEmail')) { + url = url.replace('crmEmail', lookupValue) + } + url = url.replace('ticketId', ticketId) if (type == 'api_token') { diff --git a/backend/utils/sfCase.js b/backend/utils/sfCase.js index 436357a..74ab3f8 100644 --- a/backend/utils/sfCase.js +++ b/backend/utils/sfCase.js @@ -77,6 +77,9 @@ async function sfcase(companyId, crmPhone) { if (contactId && contactId.startsWith('003')) { payload.ContactId = contactId; } + + console.log("====> contactId: ", contactId) + config.data = payload; console.log("====> create case request config: ", config) diff --git a/backend/utils/sfCaseUpdate.js b/backend/utils/sfCaseUpdate.js index d74a908..0f808b6 100644 --- a/backend/utils/sfCaseUpdate.js +++ b/backend/utils/sfCaseUpdate.js @@ -57,7 +57,7 @@ async function sfCaseUpdate(companyId, caseId, caseUpdate, agentEmail = null) { try { - // console.log("=========== payload that will be send to update the case: ", JSON.stringify(config, null, 6)) + console.log("=========== payload that will be send to update the case: ", JSON.stringify(config, null, 6)) const { data } = await axios(config) diff --git a/backend/utils/updateLeadStatus.js b/backend/utils/updateLeadStatus.js new file mode 100644 index 0000000..6bbcae7 --- /dev/null +++ b/backend/utils/updateLeadStatus.js @@ -0,0 +1,92 @@ +const axios = require('axios') +const findProperty = require('./findProperty') +const requestConfigHeader = require('./requestConfigHeader') + +/** + * Atualiza o Status de um Lead no Salesforce reutilizando as configs do template. + * - Usa a URL do createContactRecord (sobjects/Lead) para derivar o endpoint de update. + * - Reaproveita requestConfigHeader para montar headers com o token OAuth2 atual. + * + * Parâmetros: + * - rest: objeto crmRest do template carregado + * - authentication: credenciais presentes no template + * - leadId: Id do Lead (Salesforce) + * - newStatus: novo valor do campo Status (string não vazia) + * - companyId: usado em logs e controle de token + */ +async function updateLeadStatus(rest, authentication, leadId, newStatus = 'Working', companyId = '') { + + if (!leadId || typeof leadId !== 'string' || leadId.trim().length === 0) { + console.log('updateLeadStatus: leadId inválido. Abortando.') + return false + } + + if (!newStatus || typeof newStatus !== 'string' || newStatus.trim().length === 0) { + console.log('updateLeadStatus: newStatus inválido. Abortando.') + return false + } + + const createContactRecord = findProperty(rest, 'createContactRecord') + + if (!createContactRecord) { + console.log('updateLeadStatus: createContactRecord não encontrado no template. Abortando atualização de Status.') + return false + } + + const { request } = createContactRecord + + const baseUrl = request?.url + if (!baseUrl || !baseUrl.includes('/sobjects/Lead')) { + console.log('updateLeadStatus: URL base inválida para Lead. Abortando.') + return false + } + + const updateUrl = `${baseUrl}/${leadId}` + + const { type, userName, passWord, token, crmClientId } = authentication + const requestType = 'Patch' + const requestContentType = 'application/json' + + let config = await requestConfigHeader( + updateUrl, + '17999999999', + requestType, + requestContentType, + type, + userName, + passWord, + token, + crmClientId, + { Status: newStatus }, + '', + companyId + ) + + // Garante método e payload corretos + config = { ...config, ...{ method: 'Patch', url: updateUrl, data: { Status: newStatus } } } + + try { + console.log('updateLeadStatus: Enviando PATCH para atualizar Status do Lead', JSON.stringify({ url: updateUrl, data: config.data }, null, 6)) + await axios(config) + return true + } catch (error) { + if (error.response) { + console.error('==================> updateLeadStatus Erro na resposta da API:', { + status: error.response.status, + data: error.response.data, + }) + } + else if (error.request) { + console.error('==================> updateLeadStatus Nenhuma resposta recebida da API:', error.request) + } + else { + console.error('==================> updateLeadStatus Erro ao configurar a request:', error.message) + } + return false + } +} + +module.exports = updateLeadStatus + + + diff --git a/backend/utils/whatsappJournalingCRM.js b/backend/utils/whatsappJournalingCRM.js index 949a440..7bb62a0 100644 --- a/backend/utils/whatsappJournalingCRM.js +++ b/backend/utils/whatsappJournalingCRM.js @@ -1,5 +1,6 @@ const loadCRM = require('./loadCRM') const lookupContactByPhone = require('./lookupCRMContactByPhone') +const lookupContactByEmail = require('./lookupCRMContactByEmail') const createContact = require('./createContact') const findProperty = require('./findProperty') const CRM_Contact = require('../models/CRM_Contact') @@ -12,8 +13,11 @@ const sendEventTicketCreatedToSocket = require('./sendEventTicketCreatedToSocket const journalingRequest = require('./journalingRequest') const getIntegrationsConfig = require('../utils/getIntegrationsConfig') +const updateLeadStatus = require('./updateLeadStatus') +const extractLeadStatusChange = require('./extractLeadStatusChange') -async function whatsappJournalingCRM(companyId, crmPhone, crmAgent, crmFirstName = 'Username', ticketId = null) { +async function whatsappJournalingCRM(companyId, crmPhone = '', crmAgent, crmFirstName = 'Username', ticketId = null, crmEmail = '', dynamicBodyRequest = {}) { + // async function whatsappJournalingCRM(companyId, crmPhone, crmAgent, crmFirstName = 'Username', ticketId = null) { const crmFiles = await loadCRM(companyId) @@ -31,7 +35,7 @@ async function whatsappJournalingCRM(companyId, crmPhone, crmAgent, crmFirstName if (chatJournaling) { - let contact = await _lookupContact(rest, authentication, crmPhone, companyId, crmFirstName) + let contact = await _lookupContact(rest, authentication, crmPhone, crmEmail, companyId, crmFirstName) let { request, chats, response } = chatJournaling @@ -64,7 +68,20 @@ async function whatsappJournalingCRM(companyId, crmPhone, crmAgent, crmFirstName - await journalingRequest(request, body, crmCallDuration = 0, contact, crmAgent, crmPhone, authentication, rest, companyId) + await journalingRequest(request, body, crmCallDuration = 0, contact, crmAgent, crmPhone, authentication, rest, companyId, dynamicBodyRequest) + + // Se vier um novo status (__newLeadStatus), executa PATCH aproveitando as validações do fluxo de Activity + try { + const { shouldUpdate, newStatus, leadId: leadIdFromBody } = extractLeadStatusChange(dynamicBodyRequest) + const leadId = leadIdFromBody || contact?.contactId + + if (shouldUpdate && leadId) { + console.log('whatsappJournalingCRM: Atualizando Status do Lead', { leadId, newStatus }) + await updateLeadStatus(rest, authentication, leadId, newStatus, companyId) + } + } catch (err) { + console.error('whatsappJournalingCRM: falha ao atualizar Status do Lead', err?.message || err) + } } // @@ -78,19 +95,58 @@ async function whatsappJournalingCRM(companyId, crmPhone, crmAgent, crmFirstName module.exports = whatsappJournalingCRM -async function _lookupContact(rest, authentication, crmPhone, companyId, crmFirstName) { +// async function _lookupContact(rest, authentication, crmPhone, companyId, crmFirstName) { - let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) +// let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) +// if (contact?.exist) { +// return { created: false, contactId: contact.contactId } +// } + +// if (!contact?.exist) { +// contact = await createContact(companyId, rest, authentication, crmPhone, crmFirstName) +// } + +// return { created: true, contactId: contact.contactId } +// } + +async function _lookupContact(rest, authentication, crmPhone, crmEmail, companyId, crmFirstName) { + let contact = null; + + // 1. Tenta buscar o contato por telefone se o crmPhone for fornecido + if (crmPhone) { + console.log(`Buscando contato por telefone: ${crmPhone}`); + contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId); + } + + // 2. Se não encontrou por telefone, tenta buscar por e-mail se o crmEmail for fornecido + if (!contact?.exist && crmEmail) { + console.log(`Contato não encontrado por telefone. Tentando por e-mail: ${crmEmail}`); + contact = await lookupContactByEmail(rest, authentication, crmEmail, companyId); + } + + // 3. Se o contato já existe (seja por telefone ou e-mail), retorna as informações if (contact?.exist) { - return { created: false, contactId: contact.contactId } + console.log(`Contato encontrado! ID: ${contact.contactId}`); + return { created: false, contactId: contact.contactId }; } - if (!contact?.exist) { - contact = await createContact(companyId, rest, authentication, crmPhone, crmFirstName) + // 4. Se o contato não foi encontrado por nenhuma das formas, cria um novo + console.log(`Contato não encontrado. Criando um novo com base no ${crmPhone ? 'telefone' : 'e-mail'}.`); + + // AQUI ESTÁ A ÚNICA MUDANÇA NECESSÁRIA: + // Chamamos createContact passando os valores de telefone e e-mail separadamente. + // A função createContact agora sabe como lidar com ambos, mesmo que um deles seja vazio. + contact = await createContact(companyId, rest, authentication, crmPhone, crmFirstName, 'Last name', crmEmail); + + // Se o contato for criado, retorna suas informações + if (contact?.contactId) { + console.log(`Novo contato criado com ID: ${contact.contactId}`); + return { created: true, contactId: contact.contactId }; } - return { created: true, contactId: contact.contactId } + // Caso não tenha nem telefone nem e-mail, retorna um erro ou um valor nulo + console.error("Erro: Não foi possível encontrar ou criar um contato. Telefone e e-mail não fornecidos."); + return null; } -