const path = require('path') const Sentry = require('@sentry/node') const omnihitV2Integration = require('../data/omihitV2IntegrationCRM.json') const { StatusCodes } = require("http-status-codes") const { createCRMContact, sendMessageSocket, mustContainProperties, journaling, ticketCRM, findProperty, journalingRequest, createContact, lookupContactByPhone, templateValidator } = require('../utils') const fs = require("fs") const { URL } = require('url') const { oauth2 } = require('../utils') const { exchangeForTokens } = oauth2 const Company = require('../models/Company') const CRM = require('../models/CRM') const CustomError = require('../errors') const CRM_Contact = require('../models/CRM_Contact') const axios = require('axios') const { get, set } = require('../utils/redisClient') const { getContactIdChatwoot, createConversation, toggleConversationStatus } = require('../utils/ScheduleTicketCRM') const timeStamp = require('../utils/toTimestamp') const whatsappJournalingCRM = require('../utils/whatsappJournalingCRM') const redirectContactLinkCRM = require('../utils/redirectContactLinkCRM') const sfcase = require('../utils/sfCase') const sfCaseUpdate = require('../utils/sfCaseUpdate') const removeZeroInicial = require('../utils/removeZeroInicial') const contactCreate = async (req, res) => { const { companyId, crmFirstName, crmLastName, crmPhone, crmEmail } = req.body mustContainProperties(req, ['companyId', 'crmPhone',]) await createCRMContact(companyId, crmFirstName, crmPhone, crmEmail, crmLastName) res.status(StatusCodes.OK).send() } const deleteCrm = async (req, res) => { let { companyId, crmBaseURL } = req.body mustContainProperties(req, ['companyId', 'crmBaseURL',]) if (!crmBaseURL.startsWith('http://') && !crmBaseURL.startsWith('https://')) { crmBaseURL = `https://${crmBaseURL.trim()}` } const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(crmBaseURL.trim()).hostname }) if (crm) await crm.deleteOne() res.status(StatusCodes.OK).send() } const deleteCompany = async (req, res) => { const { companyId } = req.body mustContainProperties(req, ['companyId',]) const company = await Company.findOne({ companyId, }) if (company) await company.deleteOne() res.status(StatusCodes.OK).send() } const callJournaling = async (req, res) => { try { let { companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName, operationStatus } = req.body console.log(`[REQ.BODY - ${new Date()}] callJournaling: `, JSON.stringify(req.body, null, 6)) // Remove 0 from the beginning of the number. Ex: 011996067641 to 11996067641 crmPhone = removeZeroInicial(crmPhone) // Refactor this in the future. crmPhone = '55' + crmPhone console.log('========> CRMPHONE: ', crmPhone) console.log('========> COMPANY ID before: ', companyId) if (companyId == "1") companyId = '99' console.log('========> COMPANY ID after: ', companyId) if (!crmAgent) crmAgent = "0000" // mustContainProperties(req, ['companyId', 'operation', 'crmPhone', /*'crmAgent'*/,]) // if (operation == 'inboundAnsweredCall' && !crmCallDuration) // throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is inboundAnsweredCall`) // if (operation == 'outboundAsweredCall' && !crmCallDuration) // throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is outboundAsweredCall`) if (!crmCallDuration || crmCallDuration.trim() == "" || crmCallDuration == "0") crmCallDuration = "10" if (operationStatus == "hangup") await journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName) else if (operationStatus == "update-answer") { console.log('update-answer') // Referente a cliente a gabriel, pois os tickets estao sendo abertos diretamente em um botao do hitphone if (companyId != "12928") { console.log(`################## CRIAÇÃO AUTOMATICA DE TICKET COMPANY ID ${companyId} ##################`) await ticketCRM(companyId, crmPhone, crmAgent, crmFirstName) } const resp = await redirectContactLinkCRM(companyId, crmPhone, crmAgent, crmFirstName) return res.status(StatusCodes.OK).json({ contact: resp }) } res.status(StatusCodes.OK).send() } catch (error) { // console.log(`[ERROR - ${new Date()}] Erro no Call Journaling`, error?.response?.data) Sentry.captureException(error) if (error.response) { console.error('==================> callJournaling Erro na resposta da API:', { status: error.response.status, data: error.response.data, }) } else if (error.request) { console.error('==================> callJournaling Nenhuma resposta recebida da API:', error.request) } else { console.error('==================> callJournaling Erro ao configurar a request:', error.message) } res.status(StatusCodes.INTERNAL_SERVER_ERROR).send() } } const sfCreateCase = async (req, res) => { const { companyId, crmPhone } = req.body mustContainProperties(req, ['companyId', 'crmPhone',]) const sfCaseRes = await sfcase(companyId, crmPhone) res.status(StatusCodes.OK).json({ ...sfCaseRes }) } const sfUpdateCase = async (req, res) => { const { companyId, caseId, case: caseUpdate } = req.body mustContainProperties(req, ['companyId', 'caseId', 'case']) const resCaseUpdate = await sfCaseUpdate(companyId, caseId, caseUpdate) if (resCaseUpdate) res.status(StatusCodes.OK).send() if (!resCaseUpdate) res.status(StatusCodes.UNPROCESSABLE_ENTITY).send() } const install = async (req, res) => { const { authUrl, companyId } = req.query console.log('--------> authUrl: ', authUrl) // Store the authUrl in the session req.session.authUrl = authUrl + `&companyId=${companyId}` res.redirect(authUrl) } const oauthCallBack = async (req, res) => { const { code } = req.query // Retrieve the stored authUrl from the session const storedAuthUrl = req.session.authUrl console.log('xxxxxxxxxx storedAuthUrl: ', storedAuthUrl) const parsedUrl = new URL(storedAuthUrl) const clientId = parsedUrl.searchParams.get('client_id') const companyId = parsedUrl.searchParams.get('companyId') console.log('xxxxxxxxxx clientId: ', clientId) console.log('xxxxxxxxxx companyId: ', companyId) console.log('xxxxxxxxxx code: ', code) if (code) { console.log('xxxxxxxxxx passed') let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, 'companyId': companyId }) crmOauth = crmOauth.toObject() let authCodeProof = { grant_type: 'authorization_code', client_id: crmOauth.crm.authentication.crmClientId, client_secret: crmOauth.crm.authentication.crmClientSecret, redirect_uri: process.env.URL_OAUTH_CALLBACK, code } //The PKCE is mandatory for salesforce. This is for salesforce only oauth2 that need a code_challenge and code_verifier. const rest = crmOauth.crm.crmRest const salesforceUrls = rest.map(endpoint => { const key = Object.keys(endpoint)[0] const url = endpoint[key]?.request?.url if (url?.includes("salesforce")) { return `${key} URL: ${url}` } }).filter(Boolean) if (salesforceUrls.find(url => url.includes('salesforce'))) { authCodeProof = { ...authCodeProof, ...{ code_verifier: `gwkPueV2GSzkFvGFiHbNjpRuq_XBEGBsihM59pMaiFalZrOQ_7J4hgtBR8GIbLHutcuUwba2k0xeKhD8ELjWEswE3xv-H2e6Jh8EOwOccj2i_rYPUlMGdPDqpuRs8D3w`, code: `${code.split('%')[0]}` } } } // // Exchange the authorization code for an access token and refresh token await exchangeForTokens(crmOauth, authCodeProof, companyId) const url = `${process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT}?clientId=${encodeURIComponent(clientId)}&codWeb=${companyId}` return res.redirect(url) // return res.redirect(process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT) } res.status(StatusCodes.OK).send() } const testTemplate = async (req, res) => { let { clientId, companyId } = req.body let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, 'companyId': companyId, testing: true }) if (!crmOauth) return res.status(StatusCodes.OK).send() crmOauth = crmOauth.toObject() const { crmPhoneTest } = crmOauth.crm.authentication await templateValidator(crmPhoneTest, crmOauth.crm, companyId) crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, 'companyId': companyId }) crmOauth.testing = false await crmOauth.save() res.status(StatusCodes.OK).send() } const uploadCrmConfig = async (req, res) => { const { companyId, } = req.body mustContainProperties(req, ['companyId',]) //test // console.log('============> uploadCrmConfig companyId: ', companyId) // return res.send() // if (!req?.file) throw new CustomError.BadRequestError(`The crm property file must be provided`) const file = req.file let newCrm = fs.readFileSync(file.path, "utf8") newCrm = JSON.parse(newCrm) // console.log('===========> NEW CRM: ', JSON.stringify(newCrm, null, 6)) const index = newCrm.crmRest.findIndex(rest => rest?.createContactRecord) const crmBaseURL = new URL(newCrm.crmRest[index].createContactRecord.request.url.trim()).hostname let company = await Company.findOne({ companyId }) if (!company) { company = await Company.create({ companyId }) } let crm = await CRM.findOne({ companyId, crmBaseURL }) if (crm) { crm.testing = true crm.crm = newCrm await crm.save() } else { crm = await CRM.create({ crmBaseURL, companyId: company.companyId, company, testing: true, crm: newCrm }) } fs.unlinkSync(file.path) const { type, crmClientId, crmClientSecret, crmScopes, crmPhoneTest } = crm.crm.authentication if (type == 'oauth2') { const { request, body, response } = findProperty(crm.crm.crmRest, 'authorizationEndpoint') let { url } = request url = url.replace('crmClientId', crmClientId) .replace('crmClientSecret', crmClientSecret) .replace('crmScopes', crmScopes) .replace('crmRedirectURI', process.env.URL_OAUTH_CALLBACK) const authUrl = `https://api-integracao-crm.hitmanager.app.br/api/v1/crm/install?authUrl=${encodeURIComponent(url)}&companyId=${encodeURIComponent(companyId)}` console.log('--------> authUrl: ', authUrl) return res.status(StatusCodes.OK).json({ url: authUrl }) } else { await templateValidator(crmPhoneTest, newCrm, companyId) crm.testing = false await crm.save() } res.status(StatusCodes.OK).send() } const getCrms = async (req, res) => { const { companyId, } = req.params if (!companyId) throw new CustomError.BadRequestError(`The companyId must be provided in the URL`) const crms = await CRM.find({ companyId }, { _id: 1, crmBaseURL: 1, createdAt: 1, companyId: 1, enabled: 1 }) res.status(StatusCodes.OK).send(crms) } const createTicket = async (req, res) => { let { companyId, crmPhone } = req.body mustContainProperties(req, ['companyId', 'crmPhone']) const crmTicketLinks = await ticketCRM(companyId, crmPhone) .catch(function (error) { console.error(`Error on create ticket: companyID ${companyId} | crmPhone: ${crmPhone}`) console.error(error?.response?.data) console.error(error?.response?.status) console.error(error?.response?.headers) throw new Error(`Error on create ticket: companyID ${companyId} | crmPhone: ${crmPhone}`) }) return res.status(StatusCodes.OK).json({ crmTicketLinks }) } const webhook = async (req, res) => { const originIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress console.log('========> Origem da requisição IP: ', originIP) console.log('========> webhook req.body: ', JSON.stringify(req.body, null, 6)) console.log('========> req.body: ', req.body) if (!req.body?.meta) return res.status(StatusCodes.OK).send() let { name, phone_number } = req.body.meta.sender const ticketId = req.body?.id // const accountId = req.body.account_id const accountId = "15" let crmPhone = phone_number.replace('+', '') const companies = [ // { companyId: "99", account_id: "1" }, { companyId: "99", account_id: "15" }, ] const obj = companies.find(c => +c.account_id == +accountId) console.log('=======> name: ', name) console.log('=======> crmPhone: ', crmPhone) console.log('=======> accountId: ', accountId) console.log('=======> companyId: ', obj.companyId) console.log('=======> ticketId: ', ticketId) await whatsappJournalingCRM( companyId = obj.companyId, crmPhone = crmPhone, crmAgent = '0000', crmFirstName = name, ticketId ) res.status(StatusCodes.OK).send() } const webhook_crm = async (req, res) => { const originIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress console.log('========> Crm Origem da requisição IP: ', originIP) console.log('========> webhook crm req.body: ', JSON.stringify(req.body, null, 6)) const responseXml = ` true ` const sObject = req.body['soapenv:Envelope']['soapenv:Body']?.notifications?.Notification?.sObject if (!sObject) { return res.set('Content-Type', 'text/xml').status(StatusCodes.OK).send(responseXml) } const url = req.body['soapenv:Envelope']['soapenv:Body']?.notifications?.EnterpriseUrl console.log('========> salesforce url: ', url) const crm = await CRM.findOne({ crmBaseURL: new URL(url.trim()).hostname }) const { companyId } = crm console.log('========> crm companyId: ', companyId) const whoId = sObject['sf:WhoId'] const EventSubtype = sObject['sf:EventSubtype'] const StartDateTime = sObject['sf:StartDateTime'] console.log('==========> crm EventSubtype: ', EventSubtype) console.log('==========> crm StartDateTime: ', StartDateTime) console.log('==========> crm StartDateTime timeStamp: ', timeStamp(StartDateTime)) if (EventSubtype == 'Event') { const contact = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url.trim()).hostname, contactId: whoId }) console.log('==========> crm whoId: ', whoId) console.log('==========> crm contact: ', contact) if(!contact) return res.send() const { phone } = contact const obj = omnihitV2Integration.find(o => o.companyId == companyId) if (obj) { const { companyId, omnihit: { accountId, api: { url, token } = {}, createConversation: { inbox_id, status, team_id } = {} } = {} } = obj const contactIdChatwoot = await getContactIdChatwoot(url, token, phone) if (!contactIdChatwoot) { return res.set('Content-Type', 'text/xml').status(StatusCodes.OK).send(responseXml) } console.log('==========> crm contactIdChatwoot: ', contactIdChatwoot) let data = { inbox_id, contact_id: contactIdChatwoot, status, team_id } const omnihitConfig = { url, accountId, token } const ticketId = await createConversation(data, omnihitConfig) console.log('==========> crm ticketId: ', ticketId) data = { "status": "snoozed", "snoozed_until": timeStamp(StartDateTime) } await toggleConversationStatus(url, accountId, token, ticketId, data) } } return res.set('Content-Type', 'text/xml').status(StatusCodes.OK).send(responseXml) } module.exports = { contactCreate, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany, testTemplate, getCrms, webhook, webhook_crm, sfCreateCase, sfUpdateCase, createTicket }