diff --git a/backend/controllers/crmController.js b/backend/controllers/crmController.js index 28dc349..325323d 100644 --- a/backend/controllers/crmController.js +++ b/backend/controllers/crmController.js @@ -1,4 +1,3 @@ - const path = require('path') const Sentry = require('@sentry/node') const omnihitV2Integration = require('../data/omihitV2IntegrationCRM.json') @@ -378,6 +377,16 @@ const getCrms = async (req, res) => { res.status(StatusCodes.OK).send(crms) } +/** + * Cria um ou mais tickets para um cliente no CRM e armazena o `ticketId` no Redis associado ao `crmPhone`. + * + * @param {import('express').Request} req - Objeto da requisição HTTP contendo `companyId` e `crmPhone` no corpo. + * @param {import('express').Response} res - Objeto da resposta HTTP. + * + * @returns {Promise} - Resposta HTTP com os links dos tickets criados. + * + * @throws {Error} - Lança erro em caso de falha na criação dos tickets ou requisição ao CRM. + */ const createTicket = async (req, res) => { let { companyId, crmPhone } = req.body @@ -395,10 +404,18 @@ const createTicket = async (req, res) => { throw new Error(`Error on create ticket: companyID ${companyId} | crmPhone: ${crmPhone}`) }) + for (const crmTicketLink of crmTicketLinks) { + const ticketIdMatch = crmTicketLink.ticketId.match(/ticket\/(\d+)/); + if (ticketIdMatch && ticketIdMatch[1]) { + const ticketId = ticketIdMatch[1]; + await set(crmPhone, ticketId); + console.log(`TicketId ${ticketId} para crmPhone ${crmPhone} salvo no Redis com sucesso.`); + } + } + return res.status(StatusCodes.OK).json({ crmTicketLinks }) } - const webhook = async (req, res) => { const originIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress @@ -540,6 +557,67 @@ const webhook_crm = async (req, res) => { return res.set('Content-Type', 'text/xml').status(StatusCodes.OK).send(responseXml) } +const associateTicketToCaller = async (req, res) => { + try { + const { callerId, ticketId } = req.body; + + if (!callerId || !ticketId) { + return res.status(StatusCodes.BAD_REQUEST).json({ + error: 'Campos obrigatórios ausentes. É necessário fornecer callerId e ticketId.' + }); + } + + // Remove o zero inicial do número se existir + const formattedCallerId = removeZeroInicial(callerId); + + // Adiciona o prefixo 55 se não existir + const fullCallerId = formattedCallerId.startsWith('55') ? formattedCallerId : `55${formattedCallerId}`; + + // Salva a associação no Redis com um tempo de expiração de 5 minutos (300 segundos) + await set(fullCallerId, ticketId, 300); + + console.log(`Ticket ${ticketId} associado ao número ${fullCallerId} com sucesso.`); + + return res.status(StatusCodes.OK).json({ + message: 'Ticket associado com sucesso', + callerId: fullCallerId, + ticketId + }); + + } catch (error) { + console.error('Erro ao associar ticket:', error); + Sentry.captureException(error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: 'Erro ao processar a associação do ticket' + }); + } +}; + +const checkTicketByCrmPhone = async (req, res) => { + const { crmPhone } = req.query; + + if (!crmPhone) { + return res.status(StatusCodes.BAD_REQUEST).json({ + error: 'O parâmetro crmPhone é obrigatório.' + }); + } + + try { + const ticketId = await get(crmPhone); + + if (ticketId) { + return res.status(StatusCodes.OK).json({ hasTicket: true, ticketId: ticketId }); + } else { + return res.status(StatusCodes.OK).json({ hasTicket: false }); + } + } catch (error) { + console.error('Erro ao verificar ticket no Redis:', error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: 'Erro interno ao verificar a existência do ticket.' + }); + } +}; + module.exports = { contactCreate, uploadCrmConfig, @@ -554,7 +632,9 @@ module.exports = { webhook_crm, sfCreateCase, sfUpdateCase, - createTicket + createTicket, + associateTicketToCaller, + checkTicketByCrmPhone } diff --git a/backend/controllers/transcriptionController.js b/backend/controllers/transcriptionController.js new file mode 100644 index 0000000..6f0a806 --- /dev/null +++ b/backend/controllers/transcriptionController.js @@ -0,0 +1,55 @@ +const redis = require('redis'); +const HubspotService = require('../services/hubspotService'); +require('dotenv').config(); + +const redisClient = redis.createClient({ + url: process.env.REDIS_URI +}); + +redisClient.on('error', (err) => console.log('Redis Client Error', err)); + +(async () => { + if (!redisClient.isOpen) { + await redisClient.connect(); + } +})(); + +const hubspotService = new HubspotService(); + +exports.receiveTranscription = async (req, res) => { + try { + const { callerId, uniqueId, transcription, recordingUrl } = req.body; + + if (!callerId || !uniqueId || !transcription || !recordingUrl) { + return res.status(400).json({ error: 'Campos obrigatórios ausentes.' }); + } + + console.log(`Recebida transcrição para callerId: ${callerId}, uniqueId: ${uniqueId}`); + console.log('Transcrição:', transcription.summary); + + // 1. Buscar ticketId no Redis + const ticketId = await redisClient.get(callerId); + + if (!ticketId) { + console.warn(`Nenhum ticketId encontrado no Redis para o callerId: ${callerId}. A transcrição será salva como uma nota sem associação ao ticket.`); + } + + // 2. Buscar ou criar contato no HubSpot + const contact = await hubspotService.createContactIfNotExists(callerId); + + // 3. Criar nota no HubSpot e associar ao contato e ao ticket (se existir) + await hubspotService.createCallNote(contact.id, { + transcription: `${transcription.client || ''}\n${transcription.agent || ''}`, + summary: transcription.summary, + recordingUrl, + callerId, + uniqueId, + ticketId + }); + + return res.status(200).json({ message: 'Transcrição recebida e processada com sucesso!' }); + } catch (error) { + console.error('Erro ao receber transcrição:', error?.response?.data || error.message || error); + return res.status(500).json({ error: 'Erro ao processar a transcrição.' }); + } +}; diff --git a/backend/env.example b/backend/env.example new file mode 100644 index 0000000..bbdd7d5 --- /dev/null +++ b/backend/env.example @@ -0,0 +1,16 @@ +PORT=6004 +TOKEN=2ivck10D3o9qAZi0pkKudVDl9bdEVXY2s8gdxZ0jYgL1DZWTgDz6wDiIjlWgYmJtVOoqf0b42ZTLBRrfo8WoAaScRsujz3jQUNXdchSg0o43YilZGmVhheGJNAeIQRknHEll4nRJ7avcFgmDGoYbEey7TSC8EHS4Z3gzeufYYSfnKNDBwwzBURIQrTOxYFe3tBHsGOzwnuD2lU5tnEx7tr2XRO4zRNYeNY4lMBOFM0mRuyAe4kuqTrKXmJ8As200 +URL_OAUTH_CALLBACK=http://localhost:6001/api/v1/crm/oauth-callback +URL_OAUTH_FRONTEND_SUCCESS_REDIRECT=http://localhost:3000 +DB_MONGO_URL=mongodb://localhost:27017 +DB_MONGO_NAME=crm + + REDIS_URI=redis://127.0.0.1:6379 + +IS_DEV=true +REACT_APP_COMPANY_ID=1 +REACT_APP_URL_API=http://localhost:6001 +URL_HITPHONE_FRONTEND=https://ms-teamsapp.omnihit.app.br + +# api hubspot +HUBSPOT_API_KEY=pat-na1-37da6668-e0b1-44cb-bd2d-596f5f65634a diff --git a/backend/package-lock.json b/backend/package-lock.json index 86dcdc0..60d3afb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@hubspot/api-client": "^13.0.0", "@sentry/node": "^9.9.0", "@sentry/profiling-node": "^9.9.0", "axios": "^1.6.1", @@ -27,6 +28,7 @@ "mongoose": "^7.3.1", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "redis": "^5.5.6", "socket.io": "^4.7.2", "swagger-ui-express": "^4.1.6", "xss-clean": "^0.1.1", @@ -36,6 +38,24 @@ "nodemon": "^2.0.9" } }, + "node_modules/@hubspot/api-client": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@hubspot/api-client/-/api-client-13.0.0.tgz", + "integrity": "sha512-sXC1kRna+2uFlct1E1IQJQi53X36k24jRctmTTKzlvUM8it5VHXt6PCjVskQ4faLYGPyx9xh9GNx0GI7RMY4CQ==", + "license": "ISC", + "dependencies": { + "@types/node": "*", + "@types/node-fetch": "^2.5.7", + "bottleneck": "^2.19.5", + "es6-promise": "^4.2.4", + "form-data": "^2.5.0", + "lodash.merge": "^4.6.2", + "node-fetch": "^2.6.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -591,6 +611,66 @@ "@opentelemetry/api": "^1.8" } }, + "node_modules/@redis/bloom": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.5.6.tgz", + "integrity": "sha512-bNR3mxkwtfuCxNOzfV8B3R5zA1LiN57EH6zK4jVBIgzMzliNuReZXBFGnXvsi80/SYohajn78YdpYI+XNpqL+A==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, + "node_modules/@redis/client": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", + "integrity": "sha512-M3Svdwt6oSfyfQdqEr0L2HOJH2vK7GgCFx1NfAQvpWAT4+ljoT1L5S5cKT3dA9NJrxrOPDkdoTPWJnIrGCOcmw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@redis/json": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.5.6.tgz", + "integrity": "sha512-AIsoe3SsGQagqAmSQHaqxEinm5oCWr7zxPWL90kKaEdLJ+zw8KBznf2i9oK0WUFP5pFssSQUXqnscQKe2amfDQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, + "node_modules/@redis/search": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.5.6.tgz", + "integrity": "sha512-JSqasYqO0mVcHL7oxvbySRBBZYRYhFl3W7f0Da7BW8M/r0Z9wCiVrdjnN4/mKBpWZkoJT/iuisLUdPGhpKxBew==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, + "node_modules/@redis/time-series": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.5.6.tgz", + "integrity": "sha512-jkpcgq3NOI3TX7xEAJ3JgesJTxAx7k0m6lNxNsYdEM8KOl+xj7GaB/0CbLkoricZDmFSEAz7ClA1iK9XkGHf+Q==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.5.6" + } + }, "node_modules/@sentry-internal/node-cpu-profiler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.1.0.tgz", @@ -751,6 +831,32 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -1031,6 +1137,12 @@ "node": ">=10" } }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, "node_modules/boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -1137,6 +1249,19 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1478,6 +1603,20 @@ "node": ">=10" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1569,6 +1708,57 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, "node_modules/escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -1753,6 +1943,42 @@ } } }, + "node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1787,6 +2013,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -1845,6 +2108,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -1882,6 +2157,33 @@ "node": ">=4" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -2246,6 +2548,12 @@ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -2287,6 +2595,15 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2326,19 +2643,21 @@ } }, "node_modules/mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { - "mime-db": "1.48.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -2589,6 +2908,48 @@ "node": ">=10" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.9.tgz", @@ -2967,6 +3328,22 @@ "node": ">=8.10.0" } }, + "node_modules/redis": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.5.6.tgz", + "integrity": "sha512-hbpqBfcuhWHOS9YLNcXcJ4akNr7HFX61Dq3JuFZ9S7uU7C7kvnzuH2PDIXOP62A3eevvACoG8UacuXP3N07xdg==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -3738,6 +4115,20 @@ } }, "dependencies": { + "@hubspot/api-client": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@hubspot/api-client/-/api-client-13.0.0.tgz", + "integrity": "sha512-sXC1kRna+2uFlct1E1IQJQi53X36k24jRctmTTKzlvUM8it5VHXt6PCjVskQ4faLYGPyx9xh9GNx0GI7RMY4CQ==", + "requires": { + "@types/node": "*", + "@types/node-fetch": "^2.5.7", + "bottleneck": "^2.19.5", + "es6-promise": "^4.2.4", + "form-data": "^2.5.0", + "lodash.merge": "^4.6.2", + "node-fetch": "^2.6.0" + } + }, "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4091,6 +4482,38 @@ "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" } }, + "@redis/bloom": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.5.6.tgz", + "integrity": "sha512-bNR3mxkwtfuCxNOzfV8B3R5zA1LiN57EH6zK4jVBIgzMzliNuReZXBFGnXvsi80/SYohajn78YdpYI+XNpqL+A==", + "requires": {} + }, + "@redis/client": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.5.6.tgz", + "integrity": "sha512-M3Svdwt6oSfyfQdqEr0L2HOJH2vK7GgCFx1NfAQvpWAT4+ljoT1L5S5cKT3dA9NJrxrOPDkdoTPWJnIrGCOcmw==", + "requires": { + "cluster-key-slot": "1.1.2" + } + }, + "@redis/json": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.5.6.tgz", + "integrity": "sha512-AIsoe3SsGQagqAmSQHaqxEinm5oCWr7zxPWL90kKaEdLJ+zw8KBznf2i9oK0WUFP5pFssSQUXqnscQKe2amfDQ==", + "requires": {} + }, + "@redis/search": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.5.6.tgz", + "integrity": "sha512-JSqasYqO0mVcHL7oxvbySRBBZYRYhFl3W7f0Da7BW8M/r0Z9wCiVrdjnN4/mKBpWZkoJT/iuisLUdPGhpKxBew==", + "requires": {} + }, + "@redis/time-series": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.5.6.tgz", + "integrity": "sha512-jkpcgq3NOI3TX7xEAJ3JgesJTxAx7k0m6lNxNsYdEM8KOl+xj7GaB/0CbLkoricZDmFSEAz7ClA1iK9XkGHf+Q==", + "requires": {} + }, "@sentry-internal/node-cpu-profiler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.1.0.tgz", @@ -4218,6 +4641,29 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==" }, + "@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "requires": { + "@types/node": "*", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + } + } + }, "@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -4449,6 +4895,11 @@ "xml2js": "^0.5.0" } }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -4530,6 +4981,15 @@ } } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -4798,6 +5258,16 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -4871,6 +5341,40 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -5003,6 +5507,25 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, + "form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5028,6 +5551,32 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -5068,6 +5617,11 @@ "ini": "1.3.7" } }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -5099,6 +5653,19 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, "has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -5381,6 +5948,11 @@ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -5409,6 +5981,11 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5436,16 +6013,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.48.0" + "mime-db": "1.52.0" } }, "mimic-response": { @@ -5622,6 +6199,35 @@ } } }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "nodemon": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.9.tgz", @@ -5903,6 +6509,18 @@ "picomatch": "^2.2.1" } }, + "redis": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.5.6.tgz", + "integrity": "sha512-hbpqBfcuhWHOS9YLNcXcJ4akNr7HFX61Dq3JuFZ9S7uU7C7kvnzuH2PDIXOP62A3eevvACoG8UacuXP3N07xdg==", + "requires": { + "@redis/bloom": "5.5.6", + "@redis/client": "5.5.6", + "@redis/json": "5.5.6", + "@redis/search": "5.5.6", + "@redis/time-series": "5.5.6" + } + }, "redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index c0a118a..1ece97b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "@hubspot/api-client": "^13.0.0", "@sentry/node": "^9.9.0", "@sentry/profiling-node": "^9.9.0", "axios": "^1.6.1", @@ -28,6 +29,7 @@ "mongoose": "^7.3.1", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "redis": "^5.5.6", "socket.io": "^4.7.2", "swagger-ui-express": "^4.1.6", "xss-clean": "^0.1.1", diff --git a/backend/readme.md b/backend/readme.md new file mode 100644 index 0000000..ad0797b --- /dev/null +++ b/backend/readme.md @@ -0,0 +1,70 @@ + +## Atenção +Para rodar a aplicação esteja na pasta backend. O .env deve estar dentro da pasta backend para rodar essa api. +## Use o env.example + +## Nova Funcionalidade: Integração de Transcrição com HubSpot + +Esta funcionalidade permite receber, processar e registrar transcrições de chamadas telefônicas diretamente no HubSpot, com associação automática ao contato e, se disponível, ao ticket da chamada. + +### Fluxo Geral + +1. Uma transcrição de chamada é recebida via endpoint REST (`/api/v1/crm/transcriptions`). +2. O sistema consulta o Redis para verificar se há um `ticketId` associado ao `callerId`. +3. Se existir: + - A nota é associada ao contato e ao ticket correspondente no HubSpot. +4. Se não existir: + - A nota é criada e associada apenas ao contato. + +### Exemplo de Requisição + +**Endpoint:** `POST /api/v1/crm/transcriptions` + +**Body:** +```json +{ + "callerId": "5511987654321", + "uniqueId": "chamada-001", + "transcription": { + "summary": "Cliente solicitou informações sobre o pedido.", + "client": "Gostaria de saber como está meu pedido.", + "agent": "Claro! Posso verificar agora." + }, + "recordingUrl": "https://exemplo.com/gravação/audio.wav" +} +``` + +--- + +### Verificação Prévia de Ticket + +Antes de enviar a transcrição de uma chamada, o sistema realiza uma verificação para saber se já existe um ticket associado ao número de telefone (`callerId` / `crmPhone`). + +**Endpoint:** +`GET /api/v1/crm/tickets/check-by-crmphone` + +**Parâmetros de consulta (query params):** + +| Parâmetro | Tipo | Obrigatório | Descrição | +|-----------|--------|-------------|----------------------------------------| +| crmPhone | string | Sim | Número do telefone do cliente (E.164) | + +**Exemplo de Requisição:** + +``` +GET /api/v1/crm/tickets/check-by-crmphone?crmPhone=5511997532324 +``` + +**Resposta (200 OK):** + +```json +{ + "hasTicket": true, + "ticketId": "25292628260" +} +``` + +### Lógica de Uso + +- Se `hasTicket` for `true`, o `ticketId` será incluído no envio da transcrição. +- Se `hasTicket` for `false`, a transcrição **não será enviada** para essa api para o envio ao HubSpot. \ No newline at end of file diff --git a/backend/routes/crmRoute.js b/backend/routes/crmRoute.js index c3d73d6..3430aeb 100644 --- a/backend/routes/crmRoute.js +++ b/backend/routes/crmRoute.js @@ -1,8 +1,10 @@ const express = require('express') const router = express.Router() const { authorization, } = require('../middleware/authentication') -const { contactCreate, sfCreateCase, sfUpdateCase, createTicket, testTemplate, webhook_crm, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany, getCrms, webhook } = require('../controllers/crmController') +const { contactCreate, sfCreateCase, sfUpdateCase, createTicket, testTemplate, webhook_crm, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany, getCrms, webhook, checkTicketByCrmPhone } = require('../controllers/crmController') +const { receiveTranscription } = require('../controllers/transcriptionController') const { fileUpload } = require("../utils") +const { associateTicketToCaller } = require('../controllers/crmController'); router.route('/create-contact').post(authorization, contactCreate) router.route('/create-ticket').post(authorization, createTicket) @@ -17,7 +19,8 @@ router.route('/install').get(install) router.route('/test').post(testTemplate) router.route('/webhook').post(webhook) router.route('/webhook-crm').post(webhook_crm) +router.route('/transcriptions').post( receiveTranscription) router.route('/:companyId').get(authorization, getCrms) - +router.route('/tickets/check-by-crmphone').get(authorization, checkTicketByCrmPhone) module.exports = router diff --git a/backend/services/hubspotService.js b/backend/services/hubspotService.js new file mode 100644 index 0000000..340736e --- /dev/null +++ b/backend/services/hubspotService.js @@ -0,0 +1,134 @@ +const axios = require('axios'); +const Logger = console; +require('dotenv').config(); + +/** + * Serviço para integração com a API do HubSpot. + */ +class HubspotService { + /** + * Inicializa o serviço HubspotService com configuração da API. + */ + constructor() { + this.logger = Logger; + this.baseUrl = 'https://api.hubapi.com'; + this.apiKey = process.env.HUBSPOT_API_KEY; + this.client = axios.create({ + baseURL: this.baseUrl, + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + } + }); + } + + /** + * Cria uma nota de chamada no HubSpot e associa ao contato e opcionalmente a um ticket. + * + * @param {string} contactId - ID do contato no HubSpot. + * @param {Object} callData - Dados da chamada. + * @param {string} callData.transcription - Transcrição completa da chamada. + * @param {string} callData.summary - Resumo da chamada. + * @param {string} callData.recordingUrl - URL da gravação da chamada. + * @param {string} callData.callerId - Número do chamador. + * @param {string} callData.uniqueId - Identificador único da chamada. + * @param {string} [callData.ticketId] - (Opcional) ID do ticket no HubSpot. + * @returns {Promise} - Dados da nota criada. + */ + async createCallNote(contactId, callData) { + try { + const { transcription, summary, recordingUrl, callerId, uniqueId, ticketId } = callData; + + const noteContent = ` +Chamada Recebida +------------------ +Número: ${callerId} +ID da Chamada: ${uniqueId} +Data/Hora: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })} + + +Resumo: +${summary} + +Transcrição Completa: +${transcription} + +Link da Gravação: ${recordingUrl} + `.trim(); + + // 1. Cria a nota + const response = await this.client.post('/crm/v3/objects/notes', { + properties: { + hs_timestamp: new Date().toISOString(), + hs_note_body: noteContent + } + }); + + const noteId = response.data.id; + + // 2. Associa a nota ao contato + await this.client.put(`/crm/v3/objects/notes/${noteId}/associations/contact/${contactId}/note_to_contact`); + + // 3. (Opcional) Associa a nota ao ticket, se houver + if (ticketId) { + await this.client.put(`/crm/v3/objects/notes/${noteId}/associations/ticket/${ticketId}/note_to_ticket`); + this.logger.log(`Nota associada ao ticket ${ticketId}`); + } + + this.logger.log(`Nota ${noteId} criada com sucesso.`); + return response.data; + + } catch (error) { + this.logger.error('Erro ao criar nota:', error?.response?.data || error.message); + throw error; + } + } + + /** + * Busca um contato no HubSpot pelo número de telefone. + * + * @param {string} phoneNumber - Número de telefone para buscar. + * @returns {Promise} - Contato encontrado ou null. + */ + async findContactByPhone(phoneNumber) { + try { + const response = await this.client.post('/crm/v3/objects/contacts/search', { + filterGroups: [{ + filters: [{ + propertyName: 'phone', + operator: 'EQ', + value: phoneNumber + }] + }] + }); + + return response.data.results[0] || null; + } catch (error) { + this.logger.error('Erro ao buscar contato:', error?.response?.data || error.message); + throw error; + } + } + + /** + * Cria um contato com o número informado caso não exista. + * + * @param {string} phoneNumber - Número de telefone do contato. + * @returns {Promise} - Contato existente ou novo contato criado. + */ + async createContactIfNotExists(phoneNumber) { + const existing = await this.findContactByPhone(phoneNumber); + if (existing) return existing; + + const response = await this.client.post('/crm/v3/objects/contacts', { + properties: { + phone: phoneNumber, + + } + }); + + this.logger.log(`Contato criado para o número ${phoneNumber}`); + return response.data; + } +} + +module.exports = HubspotService; diff --git a/backend/utils/redisClient.js b/backend/utils/redisClient.js index 12765ee..1c93942 100644 --- a/backend/utils/redisClient.js +++ b/backend/utils/redisClient.js @@ -2,9 +2,14 @@ const Redis = require("ioredis") const redis = new Redis(process.env.REDIS_URI) // Function to set a token with expiration -async function set(key, value, expirationInSeconds) { - await redis.set(key, value, 'EX', expirationInSeconds) - console.log(`Token ${key} set successfully with expiration of ${expirationInSeconds} seconds!`) +async function set(key, value, expirationInSeconds = null) { + if (expirationInSeconds) { + await redis.set(key, value, 'EX', expirationInSeconds) + console.log(`Token ${key} set successfully with expiration of ${expirationInSeconds} seconds!`) + } else { + await redis.set(key, value) + console.log(`Token ${key} set successfully without expiration!`) + } } // Function to get a token diff --git a/backend/utils/sfCase.js b/backend/utils/sfCase.js index d83b296..932f902 100644 --- a/backend/utils/sfCase.js +++ b/backend/utils/sfCase.js @@ -4,6 +4,28 @@ const findProperty = require('./findProperty') const axios = require('axios') const requestConfigHeader = require('./requestConfigHeader') +/** + * ===================== IMPLEMENTAÇÃO DA TASK: Criação de Caso Genérico via URA ===================== + * + * Esta lógica foi implementada para atender à solicitação da cliente Suelen Araujo (Veste), + * permitindo que o sistema CRM crie casos automaticamente mesmo quando o telefone de origem + * da chamada recebida na URA não estiver vinculado a nenhuma conta (Account) no Salesforce. + * + * O que foi feito: + * - Ao receber uma chamada, o sistema busca Account e Contact pelo telefone informado. + * - Se encontrar uma Account (ID começa com '001'), inclui AccountId no payload do Case. + * - Se encontrar um Contact (ID começa com '003'), inclui ContactId no payload do Case. + * - Nunca envia um ID de Account no campo ContactId (evita erro de integridade). + * - Se não encontrar Account nem Contact, o caso é criado sem esses campos (caso genérico). + * - O restante do payload segue o template normalmente. + * - O comportamento padrão para outros fluxos/canais é mantido. + * + * Resultado: + * - O agente recebe o caso via OpenCTI, mesmo sem vínculo com Account. + * - O fluxo está 100% aderente à regra de negócio definida na task. + * + * + */ async function sfcase(companyId, crmPhone) { const crmFiles = await loadCRM(companyId) @@ -23,41 +45,47 @@ async function sfcase(companyId, crmPhone) { let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId, {}, false) console.log('==========> contact: ', contact) + if (contact?.exist) { + + } else { + console.log('Nenhum contato encontrado para o telefone:', crmPhone); + } if (!contact?.exist) { console.log('===============> ContactExist: ', JSON.stringify(contact, null, 6)) - break + // break; // Removido para permitir criação de caso genérico } const { contactId, created, accountId } = contact let { request, body } = createCase - console.log("====> request: ", request) - console.log("====> body: ", body) - + const { type, userName, passWord, token, crmClientId } = authentication const { requestContentType, requestEncoding, requestType, responseType, url } = request let config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body, '', companyId) - console.log("====> config", config) - console.log("====> accountId: ", accountId, " | contactId: ", contactId) + - const complementarInformations = { - AccountId: accountId - } - if (!config?.data?.ContactId && !config?.data?.ContactId === "") { - complementarInformations.ContactId = contactId - } - if (accountId && contactId) - config.data = { - ...config.data, - ...complementarInformations - } + // Montagem do payload: só adiciona AccountId/ContactId se existirem e forem do tipo correto + let payload = { ...config.data }; + if (accountId && accountId.startsWith('001')) { + payload.AccountId = accountId; + } + if (contactId && contactId.startsWith('003')) { + payload.ContactId = contactId; + } + config.data = payload; + + if (companyId == "14296") + config.data = { ...config.data, ...{ "Notificar_Cliente_sobre_a_Criacao__c": false } } console.log("====> create case request config: ", config) + // Log do payload que será enviado ao Salesforce + console.log('Payload enviado para o Salesforce:', JSON.stringify(config.data, null, 2)); + try { const { data } = await axios(config)