feat(transcription): add transcription handling with HubSpot integration and ticket verification

gabriel-poc
Eduxavier88 2025-06-12 17:30:46 -03:00
parent 5b9a105b7b
commit a1195810fe
9 changed files with 1005 additions and 22 deletions

View File

@ -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<import('express').Response>} - 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
}

View File

@ -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.' });
}
};

View File

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

View File

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

View File

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

70
backend/readme.md 100644
View File

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

View File

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

View File

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

View File

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