chore: initial setup with PostgreSQL, DragonflyDB, and Nginx routing configs
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.liberarSlotAgenda = liberarSlotAgenda;
|
||||
exports.notificarConflito = notificarConflito;
|
||||
exports.decrementarScore = decrementarScore;
|
||||
exports.bloquearAgendamentosAutomaticos = bloquearAgendamentosAutomaticos;
|
||||
exports.buscarScoreContato = buscarScoreContato;
|
||||
/**
|
||||
* Temporal Activities — operações com efeitos colaterais (banco, APIs, I/O).
|
||||
* Activities são executadas pelo Worker e podem ser re-tentadas automaticamente.
|
||||
*/
|
||||
const prisma_1 = require("../database/prisma");
|
||||
const logger_1 = require("../../config/logger");
|
||||
// ─── Conflito de Agenda: liberar slot após timeout ────────────────────────────
|
||||
async function liberarSlotAgenda(input) {
|
||||
logger_1.logger.info(input, '[Temporal] Slot liberado por timeout — notificando solicitante');
|
||||
// Aqui você envia mensagem via WhatsApp avisando o solicitante
|
||||
// Implementação real acoplada ao WhatsAppConnectionManager via NATS
|
||||
// Por ora, marca no banco como disponível
|
||||
await prisma_1.prisma.protocol.updateMany({
|
||||
where: {
|
||||
contactId: input.solicitanteId,
|
||||
status: 'WAITING_CLIENT',
|
||||
},
|
||||
data: { status: 'OPEN' },
|
||||
});
|
||||
}
|
||||
async function notificarConflito(input) {
|
||||
logger_1.logger.info(input, '[Temporal] Notificando pessoa B sobre solicitação de slot');
|
||||
// Dispara mensagem via NATS → WhatsApp handler
|
||||
}
|
||||
// ─── Gestão de Reputação ──────────────────────────────────────────────────────
|
||||
async function decrementarScore(input) {
|
||||
const contact = await prisma_1.prisma.contact.findUnique({
|
||||
where: { id: input.contactId },
|
||||
});
|
||||
if (!contact)
|
||||
throw new Error(`Contato ${input.contactId} não encontrado`);
|
||||
const novoScore = Math.max(0, contact.scoreReputacao - input.pontos);
|
||||
const restrito = novoScore <= 20;
|
||||
await prisma_1.prisma.contact.update({
|
||||
where: { id: input.contactId },
|
||||
data: {
|
||||
scoreReputacao: novoScore,
|
||||
flagRestricao: restrito,
|
||||
},
|
||||
});
|
||||
logger_1.logger.info({ contactId: input.contactId, novoScore, restrito, motivo: input.motivo }, '[Temporal] Score de reputação atualizado');
|
||||
return { novoScore, restrito };
|
||||
}
|
||||
async function bloquearAgendamentosAutomaticos(input) {
|
||||
await prisma_1.prisma.contact.update({
|
||||
where: { id: input.contactId },
|
||||
data: { flagRestricao: true },
|
||||
});
|
||||
logger_1.logger.warn({ contactId: input.contactId }, '[Temporal] Contato marcado como RESTRITO — agendamentos automáticos bloqueados');
|
||||
}
|
||||
async function buscarScoreContato(contactId) {
|
||||
const c = await prisma_1.prisma.contact.findUnique({
|
||||
where: { id: contactId },
|
||||
select: { scoreReputacao: true, flagRestricao: true },
|
||||
});
|
||||
return { score: c?.scoreReputacao ?? 100, restrito: c?.flagRestricao ?? false };
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getTemporalClient = getTemporalClient;
|
||||
const client_1 = require("@temporalio/client");
|
||||
const env_1 = require("../../config/env");
|
||||
const logger_1 = require("../../config/logger");
|
||||
let _client = null;
|
||||
async function getTemporalClient() {
|
||||
if (_client)
|
||||
return _client;
|
||||
const connection = await client_1.Connection.connect({ address: env_1.env.TEMPORAL_ADDRESS });
|
||||
_client = new client_1.Client({
|
||||
connection,
|
||||
namespace: env_1.env.TEMPORAL_NAMESPACE,
|
||||
});
|
||||
logger_1.logger.info({ address: env_1.env.TEMPORAL_ADDRESS }, 'Temporal Client conectado');
|
||||
return _client;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* Worker do Temporal — roda como processo separado.
|
||||
* Uso: npx tsx src/infra/temporal/temporalWorker.ts
|
||||
*/
|
||||
const worker_1 = require("@temporalio/worker");
|
||||
const env_1 = require("../../config/env");
|
||||
const logger_1 = require("../../config/logger");
|
||||
const activities = __importStar(require("./activities"));
|
||||
async function runWorker() {
|
||||
const worker = await worker_1.Worker.create({
|
||||
workflowsPath: require.resolve('./workflows'),
|
||||
activities,
|
||||
taskQueue: env_1.env.TEMPORAL_TASK_QUEUE,
|
||||
namespace: env_1.env.TEMPORAL_NAMESPACE,
|
||||
});
|
||||
logger_1.logger.info({ taskQueue: env_1.env.TEMPORAL_TASK_QUEUE }, '🕰 Temporal Worker iniciado');
|
||||
await worker.run();
|
||||
}
|
||||
runWorker().catch((err) => {
|
||||
logger_1.logger.error(err, 'Falha fatal no Temporal Worker');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.confirmarSlotSignal = void 0;
|
||||
exports.conflitoAgendaWorkflow = conflitoAgendaWorkflow;
|
||||
/**
|
||||
* Workflow: Conflito de Agenda (1 Hora)
|
||||
*
|
||||
* Regra de negócio:
|
||||
* - Pessoa A solicita o slot que Pessoa B já tem PENDENTE.
|
||||
* - Sistema marca o conflito como WAITING_B e aguarda 1 hora.
|
||||
* - Se B confirmar dentro de 1h → slot permanece com B, A é notificada.
|
||||
* - Se B não responder em 1h → slot é liberado e transferido para A.
|
||||
*/
|
||||
const workflow_1 = require("@temporalio/workflow");
|
||||
const { liberarSlotAgenda, notificarConflito, } = (0, workflow_1.proxyActivities)({
|
||||
startToCloseTimeout: '30 seconds',
|
||||
retry: { maximumAttempts: 3 },
|
||||
});
|
||||
// Signal enviado quando B confirma o slot
|
||||
exports.confirmarSlotSignal = (0, workflow_1.defineSignal)('confirmarSlot');
|
||||
async function conflitoAgendaWorkflow(input) {
|
||||
let bConfirmou = false;
|
||||
// Registra o handler do signal de confirmação de B
|
||||
(0, workflow_1.setHandler)(exports.confirmarSlotSignal, ({ confirmado }) => {
|
||||
bConfirmou = confirmado;
|
||||
workflow_1.log.info('Signal recebido de B', { confirmado });
|
||||
});
|
||||
// Notifica B sobre a disputa
|
||||
await notificarConflito({
|
||||
chatIdB: input.chatIdDetentor,
|
||||
solicitanteNome: input.solicitanteId,
|
||||
tituloSlot: input.tituloSlot,
|
||||
});
|
||||
workflow_1.log.info('Aguardando confirmação de B por 1 hora', { tituloSlot: input.tituloSlot });
|
||||
// Aguarda signal de B OU timeout de 1 hora
|
||||
const bRespondeu = await (0, workflow_1.condition)(() => bConfirmou !== false, '1 hour');
|
||||
if (bRespondeu && bConfirmou) {
|
||||
workflow_1.log.info('B confirmou o slot — slot mantido com B');
|
||||
return { resultado: 'MANTIDO_B' };
|
||||
}
|
||||
// B não respondeu ou recusou → libera para A
|
||||
workflow_1.log.info('B não confirmou em 1 hora — liberando slot para A');
|
||||
await liberarSlotAgenda({
|
||||
solicitanteId: input.solicitanteId,
|
||||
tituloSlot: input.tituloSlot,
|
||||
chatIdSolicitante: input.chatIdSolicitante,
|
||||
});
|
||||
return { resultado: 'LIBERADO_A' };
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encerrarMonitoramentoSignal = exports.registrarInfracaoSignal = exports.reputacaoWorkflow = exports.confirmarSlotSignal = exports.conflitoAgendaWorkflow = void 0;
|
||||
var conflitoAgendaWorkflow_1 = require("./conflitoAgendaWorkflow");
|
||||
Object.defineProperty(exports, "conflitoAgendaWorkflow", { enumerable: true, get: function () { return conflitoAgendaWorkflow_1.conflitoAgendaWorkflow; } });
|
||||
Object.defineProperty(exports, "confirmarSlotSignal", { enumerable: true, get: function () { return conflitoAgendaWorkflow_1.confirmarSlotSignal; } });
|
||||
var reputacaoWorkflow_1 = require("./reputacaoWorkflow");
|
||||
Object.defineProperty(exports, "reputacaoWorkflow", { enumerable: true, get: function () { return reputacaoWorkflow_1.reputacaoWorkflow; } });
|
||||
Object.defineProperty(exports, "registrarInfracaoSignal", { enumerable: true, get: function () { return reputacaoWorkflow_1.registrarInfracaoSignal; } });
|
||||
Object.defineProperty(exports, "encerrarMonitoramentoSignal", { enumerable: true, get: function () { return reputacaoWorkflow_1.encerrarMonitoramentoSignal; } });
|
||||
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encerrarMonitoramentoSignal = exports.registrarInfracaoSignal = void 0;
|
||||
exports.reputacaoWorkflow = reputacaoWorkflow;
|
||||
/**
|
||||
* Workflow: Gestão de Reputação de Contato
|
||||
*
|
||||
* Regra de negócio:
|
||||
* - Cada falta/infração reduz o score_reputacao do contato.
|
||||
* - Score ≤ 20 → contato entra em flag_restricao = true.
|
||||
* - Agendamentos automáticos são bloqueados para contatos RESTRITOS.
|
||||
* - Score é recuperável manualmente pelo agente (fora deste workflow).
|
||||
*/
|
||||
const workflow_1 = require("@temporalio/workflow");
|
||||
const { decrementarScore, bloquearAgendamentosAutomaticos, buscarScoreContato, } = (0, workflow_1.proxyActivities)({
|
||||
startToCloseTimeout: '30 seconds',
|
||||
retry: { maximumAttempts: 3 },
|
||||
});
|
||||
// Signal para registrar uma nova infração
|
||||
exports.registrarInfracaoSignal = (0, workflow_1.defineSignal)('registrarInfracao');
|
||||
// Signal para encerrar o monitoramento (contato reabilitado manualmente)
|
||||
exports.encerrarMonitoramentoSignal = (0, workflow_1.defineSignal)('encerrarMonitoramento');
|
||||
async function reputacaoWorkflow(input) {
|
||||
let encerrado = false;
|
||||
const infracoesPendentes = [];
|
||||
(0, workflow_1.setHandler)(exports.registrarInfracaoSignal, (payload) => {
|
||||
workflow_1.log.info('Infração registrada via signal', payload);
|
||||
infracoesPendentes.push(payload);
|
||||
});
|
||||
(0, workflow_1.setHandler)(exports.encerrarMonitoramentoSignal, () => {
|
||||
encerrado = true;
|
||||
});
|
||||
// Loop durável — o Temporal mantém o estado mesmo após reinícios
|
||||
while (!encerrado) {
|
||||
// Aguarda próxima infração ou encerramento
|
||||
await (0, workflow_1.condition)(() => infracoesPendentes.length > 0 || encerrado);
|
||||
if (encerrado)
|
||||
break;
|
||||
const infracao = infracoesPendentes.shift();
|
||||
const { novoScore, restrito } = await decrementarScore({
|
||||
contactId: input.contactId,
|
||||
motivo: infracao.motivo,
|
||||
pontos: infracao.pontos,
|
||||
});
|
||||
workflow_1.log.info('Score atualizado', { novoScore, restrito, contactId: input.contactId });
|
||||
if (restrito) {
|
||||
await bloquearAgendamentosAutomaticos({ contactId: input.contactId });
|
||||
workflow_1.log.warn('Contato RESTRITO — agendamentos automáticos bloqueados', {
|
||||
contactId: input.contactId,
|
||||
scoreAtual: novoScore,
|
||||
});
|
||||
// Continua monitorando (restrição é revertida manualmente pelo agente)
|
||||
}
|
||||
}
|
||||
workflow_1.log.info('Monitoramento de reputação encerrado', { contactId: input.contactId });
|
||||
}
|
||||
Reference in New Issue
Block a user