From 7af9e34ad73c587ec292c5c5a2cbd824b0c3143a Mon Sep 17 00:00:00 2001 From: adriano Date: Fri, 19 Jul 2024 15:48:34 -0300 Subject: [PATCH] feat: add support for ticket creation --- .../hubspot_bearer_auth_editind_ticket.json | 164 +++++++++++------- backend/controllers/crmController.js | 17 +- backend/models/CRM.js | 10 ++ backend/models/CRM_Contact.js | 15 ++ backend/models/CRM_Ticket.js | 29 ++++ backend/utils/createTicket.js | 83 +++++++++ backend/utils/findProperty.js | 4 +- backend/utils/index.js | 5 +- backend/utils/lookupCRMContactByPhone.js | 6 +- backend/utils/lookupCRMTicket.js | 88 ++++++++++ backend/utils/requestConfigHeader.js | 4 +- backend/utils/ticketCRM.js | 100 +++++++++++ 12 files changed, 457 insertions(+), 68 deletions(-) create mode 100644 backend/models/CRM_Ticket.js create mode 100644 backend/utils/createTicket.js create mode 100644 backend/utils/lookupCRMTicket.js create mode 100644 backend/utils/ticketCRM.js diff --git a/backend/Templates-test/hubspot_bearer_auth_editind_ticket.json b/backend/Templates-test/hubspot_bearer_auth_editind_ticket.json index 0efc058..860332e 100644 --- a/backend/Templates-test/hubspot_bearer_auth_editind_ticket.json +++ b/backend/Templates-test/hubspot_bearer_auth_editind_ticket.json @@ -1,57 +1,57 @@ { - "authentication":{ - "type": "bearer", - "token": "pat-na1-7aca13dd-9ba5-48db-bf35-570844d31abb", - "crmPhoneTest": "5511988334455" - }, - "crmRest":[ - { - "createContactRecord":{ - "request":{ - "requestContentType":"application/json", - "requestEncoding":"Json", - "requestType":"Post", - "responseType":"Json", - "url":"https://api.hubapi.com/contacts/v1/contact" - }, - "body":{ - "properties":[ - { - "property":"phone", - "value":"crmPhone" - } - ] - }, - "response":{ - "id":"vid" + "authentication": { + "type": "bearer", + "token": "pat-na1-37da6668-e0b1-44cb-bd2d-596f5f65634a", + "crmPhoneTest": "5514987659932" + }, + "crmRest": [ + { + "createContactRecord": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Post", + "responseType": "Json", + "url": "https://api.hubapi.com/contacts/v1/contact" + }, + "body": { + "properties": [ + { + "property": "phone", + "value": "crmPhone" } - } - }, - { - "lookupContactByPhone":{ - "request":{ - "requestContentType":"application/json", - "requestEncoding":"Json", - "requestType":"Get", - "responseType":"Json", - "url":"https://api.hubapi.com/contacts/v1/search/query?q=crmPhone" - }, - "response":{ - "phone":"contacts.properties.phone.value", - "id":"contacts.vid" - } - } - }, - { - "callJournaling":{ - "request":{ - "requestContentType":"application/json", - "requestEncoding":"Json", - "requestType":"Post", - "responseType":"Json", - "url":"https://api.hubapi.com/engagements/v1/engagements" - }, - "calls": [ + ] + }, + "response": { + "id": "vid" + } + } + }, + { + "lookupContactByPhone": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Get", + "responseType": "Json", + "url": "https://api.hubapi.com/contacts/v1/search/query?q=crmPhone" + }, + "response": { + "phone": "contacts.properties.phone.value", + "id": "contacts.vid" + } + } + }, + { + "callJournaling": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Post", + "responseType": "Json", + "url": "https://api.hubapi.com/engagements/v1/engagements" + }, + "calls": [ { "inboundAnsweredCall": { "engagement": { @@ -179,10 +179,58 @@ } } ], - "response":{ - - } - } + "response": {} } - ] -} \ No newline at end of file + }, + { + "createTicketRecord": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Post", + "responseType": "Json", + "url": "https://api.hubapi.com/crm/v3/objects/tickets" + }, + "body": { + "properties": { + "hs_pipeline": "0", + "hs_pipeline_stage": "1", + "hs_ticket_priority": "HIGH", + "subject": "Teste" + }, + "associations": [ + { + "to": { + "id": "crmContactId" + }, + "types": [ + { + "associationCategory": "HUBSPOT_DEFINED", + "associationTypeId": 16 + } + ] + } + ] + }, + "response": { + "id": "id" + } + } + }, + { + "lookupTicket": { + "request": { + "requestContentType": "application/json", + "requestEncoding": "Json", + "requestType": "Get", + "responseType": "Json", + "url": "https://api.hubapi.com/crm/v3/objects/tickets/ticketId" + }, + "response": { + "status": "properties.hs_pipeline_stage", + "id": "id" + } + } + } + ] +} diff --git a/backend/controllers/crmController.js b/backend/controllers/crmController.js index 95aa462..a9e60a2 100644 --- a/backend/controllers/crmController.js +++ b/backend/controllers/crmController.js @@ -5,6 +5,7 @@ const { createCRMContact, sendMessageSocket, mustContainProperties, journaling, + ticketCRM, findProperty, journalingRequest, createContact, @@ -60,7 +61,9 @@ const deleteCompany = async (req, res) => { const callJournaling = async (req, res) => { - const { companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName } = req.body + const { companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName, operationStatus } = req.body + + console.log('REQ.BODY CRM TEST: ', JSON.stringify(req.body, null, 6)) mustContainProperties(req, ['companyId', 'operation', 'crmPhone', 'crmAgent',]) @@ -69,11 +72,15 @@ const callJournaling = async (req, res) => { // if (operation == 'outboundAsweredCall' && !crmCallDuration) // throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is outboundAsweredCall`) - await journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName) + if (operationStatus == "hangup") + await journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration, crmFirstName) + else if (operationStatus == "answered") { + await ticketCRM(companyId, crmPhone, crmAgent, crmFirstName) + } res.status(StatusCodes.OK).send() -} - +} + const install = async (req, res) => { const { authUrl, companyId } = req.query @@ -224,7 +231,7 @@ module.exports = { install, deleteCrm, deleteCompany, - testTemplate + testTemplate } diff --git a/backend/models/CRM.js b/backend/models/CRM.js index fb0a7c0..ca5515b 100644 --- a/backend/models/CRM.js +++ b/backend/models/CRM.js @@ -63,6 +63,15 @@ const CRMRestSchema = new Schema({ request: RequestSchema, response: Object }, + createTicketRecord: { + request: RequestSchema, + body: Object, + response: Object + }, + lookupTicket: { + request: RequestSchema, + response: Object + }, callJournaling: CallJournalingSchema }) @@ -125,6 +134,7 @@ crmContactSchema.virtual('crm_contacts', { crmContactSchema.pre('deleteOne', { document: true, query: false }, async function () { await this.model('CRM_Contact').deleteMany({ crm: this._id }) + // await this.model('CRM_Ticket').deleteMany({ crm: this._id }) } ) diff --git a/backend/models/CRM_Contact.js b/backend/models/CRM_Contact.js index 9314f17..3c229fa 100644 --- a/backend/models/CRM_Contact.js +++ b/backend/models/CRM_Contact.js @@ -26,6 +26,21 @@ const crmContactSchema = new Schema({ } }, { timestamps: true }) + +crmContactSchema.virtual('crm_tickets', { + ref: 'CRM_Ticket', + localField: '_id', + foreignField: 'contact', + justOne: false, + // match: { rating: 4 } +}) + + +crmContactSchema.pre('deleteOne', { document: true, query: false }, async function () { + await this.model('CRM_Ticket').deleteMany({ contact: this._id }) +} +) + const CRM_Contact = mongoose.model('CRM_Contact', crmContactSchema) module.exports = CRM_Contact diff --git a/backend/models/CRM_Ticket.js b/backend/models/CRM_Ticket.js new file mode 100644 index 0000000..a7ad280 --- /dev/null +++ b/backend/models/CRM_Ticket.js @@ -0,0 +1,29 @@ +const mongoose = require('../db/connect') + +const { Schema } = mongoose + +const crmTicketSchema = new Schema({ + companyId: { + type: String, + required: true, + }, + crm: { + type: mongoose.Schema.ObjectId, + ref: 'crm', + required: true + }, + ticketId: { + type: String, + required: true, + }, + contact: { + type: mongoose.Schema.ObjectId, + ref: 'contact', + required: true + }, +}, { timestamps: true }) + + +const CRM_Ticket = mongoose.model('CRM_Ticket', crmTicketSchema) + +module.exports = CRM_Ticket diff --git a/backend/utils/createTicket.js b/backend/utils/createTicket.js new file mode 100644 index 0000000..319b01e --- /dev/null +++ b/backend/utils/createTicket.js @@ -0,0 +1,83 @@ +const flatten = require('flat') +const unflatten = require('flat').unflatten +const axios = require('axios') +const CRM_Contact = require('../models/CRM_Contact') +const CRM_Ticket = require('../models/CRM_Ticket') +const { URL } = require('url') +const findProperty = require('./findProperty') +const CRM = require('../models/CRM') +const requestConfigHeader = require('./requestConfigHeader') +const sendMessageSocket = require('./sendMessageSocket') + +async function createTicket(companyId, rest, authentication, crmPhone, crmFirstName = 'unnamed', crmLastName = 'no surname', crmEmail = '', test = {}, crmContactId) { + + let { request, body, response } = findProperty(rest, 'createTicketRecord') + + const { requestContentType, requestEncoding, requestType, responseType, url } = request + + + body = flatten(body) + + const mapping = { + crmFirstName, + crmLastName, + crmPhone, + crmEmail, + crmContactId + } + + for (const prop in body) { + if (mapping.hasOwnProperty(body[prop])) { + const variable = mapping[body[prop]] + if (variable) { + body[prop] = variable + } else { + if (body[prop] == 'crmLastName' && !crmLastName) { + body[prop] = 'unnamed' + } + else + delete body[prop] + } + } + } + + body = unflatten(body) + + const { type, userName, passWord, token, crmClientId } = authentication + + //url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '' + const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body) + + if (test?.testing) { + msg = `Tentanto criar ticket do numero ${crmPhone} no crm` + sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } }) + } + + let { data } = await axios(config) + + data = flatten(data) + + let auxTicketId + + for (const prop in data) { + + const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '') + + if (_prop == response?.id?.trim()) { + auxTicketId = data[prop] + break + } + } + + if (auxTicketId && !test?.testing) { + const contact = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, phone: crmPhone }) + const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname }) + + await CRM_Ticket.create({ companyId, contact, ticketId: auxTicketId, crm}) + } + + return { exist: true, ticketId: auxTicketId, phone: crmPhone } + +} + +module.exports = createTicket \ No newline at end of file diff --git a/backend/utils/findProperty.js b/backend/utils/findProperty.js index 151f0aa..259a835 100644 --- a/backend/utils/findProperty.js +++ b/backend/utils/findProperty.js @@ -3,7 +3,9 @@ function findProperty(array, property) { const index = array.findIndex((item) => { return item[property] !== undefined - }) + }) + + if (index == -1) return return array[index][property] } diff --git a/backend/utils/index.js b/backend/utils/index.js index 6497546..5f9035b 100644 --- a/backend/utils/index.js +++ b/backend/utils/index.js @@ -24,6 +24,8 @@ const lookupContactByPhone = require('./lookupCRMContactByPhone') const socketIO = require('./socketIO') const sendMessageSocket = require('./sendMessageSocket') const templateValidator = require('./templateValidator') +const ticketCRM = require('./ticketCRM') + module.exports = { fileUpload, @@ -46,5 +48,6 @@ module.exports = { lookupContactByPhone, socketIO, sendMessageSocket, - templateValidator + templateValidator, + ticketCRM } diff --git a/backend/utils/lookupCRMContactByPhone.js b/backend/utils/lookupCRMContactByPhone.js index c91957e..f1bb38f 100644 --- a/backend/utils/lookupCRMContactByPhone.js +++ b/backend/utils/lookupCRMContactByPhone.js @@ -32,6 +32,8 @@ async function lookupContactByPhone(rest, authentication, crmPhone, companyId, t let { data } = await axios(config) + console.log('DATA: ', JSON.stringify(data, null, 6)) + data = flatten(data) let auxPhone @@ -69,7 +71,7 @@ async function lookupContactByPhone(rest, authentication, crmPhone, companyId, t if (auxPhone && auxContactId) break } - } + } if (auxPhone) { @@ -81,7 +83,7 @@ async function lookupContactByPhone(rest, authentication, crmPhone, companyId, t return { exist: true, contactId: auxContactId, phone: crmPhone } } - return { exit: false } + return { exist: false } } module.exports = lookupContactByPhone diff --git a/backend/utils/lookupCRMTicket.js b/backend/utils/lookupCRMTicket.js new file mode 100644 index 0000000..7a00ddf --- /dev/null +++ b/backend/utils/lookupCRMTicket.js @@ -0,0 +1,88 @@ +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') + +async function lookupCrmTicket(rest, authentication, crmPhone, companyId, test = {}, ticketId) { + + let { request, body, response } = findProperty(rest, 'lookupTicket') + + let { requestContentType, requestEncoding, requestType, responseType, url } = request + + + const { type, userName, passWord, token, crmClientId } = authentication + + const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, '', ticketId) + + if (test?.testing) { + let msg = `Tentanto checar o status do ticket para o numer ${crmPhone} no crm` + sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } }) + } + + let resp + + try { + resp = await axios(config) + } catch (error) { + if (error?.response?.status == 404) + return { error: 404 } + } + + let { data } = resp + + data = flatten(data) + + let auxTicketStatus + let auxTicketId + + for (const prop in data) { + + const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '') + + if (_prop == response?.status?.trim()) { + auxTicketStatus = data[prop].replace('+', '') + } + + if (_prop == response?.id?.trim()) { + auxTicketId = data[prop] + } + + if (auxTicketStatus && auxTicketId) break + + } + + if (!auxTicketStatus && !auxTicketId) { + for (const prop in data) { + + let _prop = prop.replace(/\.(\d+)(\.|$)/g, '[$1]$2') + + if (_prop == response?.status?.trim()) { + auxTicketStatus = data[prop].replace('+', '') + } + + if (_prop == response?.id?.trim()) { + auxTicketId = data[prop] + } + + if (auxTicketStatus && auxTicketId) break + + } + } + + console.log('auxTicketStatus: ', auxTicketStatus, ' | auxTicketId: ', auxTicketId) + + return { auxTicketId, auxTicketStatus } + +} + +module.exports = lookupCrmTicket + + diff --git a/backend/utils/requestConfigHeader.js b/backend/utils/requestConfigHeader.js index 6f67786..cf7e637 100644 --- a/backend/utils/requestConfigHeader.js +++ b/backend/utils/requestConfigHeader.js @@ -1,9 +1,11 @@ const { getAccessToken } = require('./oauth2') -async function requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '') { +async function requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '', ticketId='') { let config = {} + console.log('requestConfigHeader ticketId: ', ticketId) url = url.replace('crmPhone', crmPhone) + url = url.replace('ticketId', ticketId) if (type == 'api_token') { if (requestType.trim().toLowerCase() == 'post') { diff --git a/backend/utils/ticketCRM.js b/backend/utils/ticketCRM.js new file mode 100644 index 0000000..fd8ae47 --- /dev/null +++ b/backend/utils/ticketCRM.js @@ -0,0 +1,100 @@ +const loadCRM = require('./loadCRM') +const lookupContactByPhone = require('./lookupCRMContactByPhone') +const createContact = require('./createContact') +const findProperty = require('./findProperty') +const CRM_Contact = require('../models/CRM_Contact') +const CRM_Ticket = require('../models/CRM_Ticket') +const CRM = require('../models/CRM') +const createTicket = require('./createTicket') + +const lookupCRMTicket = require('./lookupCRMTicket') + + +async function ticketCRM(companyId, crmPhone, crmAgent, crmFirstName = 'unnamed') { + + const crmFiles = await loadCRM(companyId) + + for (const crmConfig of crmFiles) { + + const { crmRest: rest, authentication } = crmConfig.crm + + let obj = findProperty(rest, 'lookupTicket') + + if (obj) { + + let { url } = obj.request + let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId) + + if (!contact?.exist) { + contact = await createContact(companyId, rest, authentication, crmPhone, crmFirstName) + } + + const crm = await CRM.findOne({ + companyId, crmBaseURL: new URL(url.trim()).hostname + }) + + const obj_contact = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, phone: crmPhone }) + + const obj_ticket = await CRM_Ticket.findOne( + { companyId, crm, contact: obj_contact } + ).select('ticketId') + + if (obj_ticket) { + + const obj_ticket_status = await lookupCRMTicket( + rest, + authentication, + crmPhone, + companyId, + test = { testing: false }, + obj_ticket.ticketId + ) + + if (obj_ticket_status) { + const { auxTicketStatus, error } = obj_ticket_status + + // refactor this for production. For now only working with hubspot where status new is equal 1 + if ((auxTicketStatus && auxTicketStatus != '1') || (error && error == 404)) { + await CRM_Ticket.deleteMany({ contact: obj_contact }) + crmFirstName = await _createTicket(rest, crmPhone, companyId, authentication, crmFirstName, contact) + } + } + + } + else { + crmFirstName = await _createTicket(rest, crmPhone, companyId, authentication, crmFirstName, contact) + } + + } + + } + +} + +module.exports = ticketCRM + + +async function _createTicket(rest, crmPhone, companyId, authentication, crmFirstName, contact) { + let obj = findProperty(rest, 'createTicketRecord') + + if (obj) { + let { request, response } = obj + + if (request) { + msg = `Tentando criar ticket para o contato ${crmPhone}` + + await createTicket(companyId, + rest, + authentication, + crmPhone, + crmFirstName = 'unnamed', + crmLastName = '', + crmEmail = '', + test = { testing: false }, + crmContactId = contact.contactId + ) + } + } + return crmFirstName +} +