adriano 2025-10-09 15:06:25 -03:00
commit a9ee98449b
14 changed files with 611 additions and 50 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -17,7 +17,7 @@ const { createCRMContact,
const fs = require("fs")
const { URL } = require('url')
const { oauth2 } = require('../utils')
const { exchangeForTokens } = oauth2
const { exchangeForTokens, getAccessToken } = oauth2
const Company = require('../models/Company')
const CRM = require('../models/CRM')
const CustomError = require('../errors')
@ -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
// mustContainProperties(req, ['companyId', 'crmPhone', 'ticketId'])
// await whatsappJournalingCRM(
// companyId,
// crmPhone,
// '0000',
// crmFirstName = "unnamed",
// ticketId
// )
// res.status(StatusCodes.OK).send()
// }
const contactActivity = async (req, res) => {
const { companyId, crmPhone, ticketId } = req.body
// 1. Recebe 'crmEmail' do corpo da requisição
const { companyId, crmPhone, crmEmail, ticketId, dynamicBodyRequest } = req.body;
mustContainProperties(req, ['companyId', 'crmPhone', 'ticketId'])
// 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) => {
@ -260,8 +321,6 @@ const oauthCallBack = async (req, res) => {
if (code) {
console.log('xxxxxxxxxx passed')
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, 'companyId': companyId })
crmOauth = crmOauth.toObject()
@ -593,6 +652,26 @@ const webhook_crm = async (req, res) => {
return res.set('Content-Type', 'text/xml').status(StatusCodes.OK).send(responseXml)
}
const getClientAccessToken = async (req, res) => {
const { companyId } = req.params
const { clientId } = req.query
console.log('========> getClientAccessToken companyId: ', companyId, ' clientId: ', clientId);
if (!companyId || !clientId) {
console.error('Company ID or Client ID is missing in the request parameters.');
return res.status(StatusCodes.BAD_REQUEST).send({ msg: "Company ID and Client ID are required!" });
}
const accessToken = await getAccessToken(clientId, companyId);
if (!accessToken) {
console.error(`Access token not found for companyId: ${companyId} and clientId: ${clientId}`);
return res.status(StatusCodes.NOT_FOUND).send({ msg: "Access token not found!" });
}
return res.status(StatusCodes.OK).json({ accessToken });
}
module.exports = {
contactCreate,
uploadCrmConfig,
@ -609,7 +688,8 @@ module.exports = {
sfUpdateCase,
createTicket,
checkContact,
contactActivity
contactActivity,
getClientAccessToken
}

View File

@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const { authorization, } = require('../middleware/authentication')
const { contactCreate, contactActivity, checkContact, sfCreateCase, sfUpdateCase, createTicket, testTemplate, webhook_crm, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany, getCrms, webhook } = require('../controllers/crmController')
const { contactCreate, contactActivity, checkContact, sfCreateCase, sfUpdateCase, createTicket, testTemplate, webhook_crm, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany, getCrms, webhook, getClientAccessToken } = require('../controllers/crmController')
const { fileUpload } = require("../utils")
router.route('/create-contact').post(authorization, contactCreate)
@ -22,6 +22,7 @@ router.route('/test').post(testTemplate)
router.route('/webhook').post(webhook)
router.route('/webhook-crm').post(webhook_crm)
router.route('/:companyId').get(authorization, getCrms)
router.route('/:companyId/access-token').get(authorization, getClientAccessToken)
module.exports = router

View File

@ -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)

View File

@ -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 }

View File

@ -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

View File

@ -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 } })
}

View File

@ -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<object>} 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

View File

@ -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') {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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;
}