Files
scoreodonto.com/backend/dist/modules/scheduled/scheduled.routes.js
T

231 lines
11 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildScheduledRoutes = buildScheduledRoutes;
/**
* Rotas de Agendamentos de Mensagem
*
* GET /api/scheduled — Lista agendamentos do tenant
* POST /api/scheduled — Cria agendamento
* GET /api/scheduled/:id — Detalhe
* PUT /api/scheduled/:id — Atualiza
* DELETE /api/scheduled/:id — Remove
* POST /api/scheduled/:id/pause — Pausa
* POST /api/scheduled/:id/resume — Retoma
* POST /api/scheduled/:id/run-now — Executa imediatamente
* GET /api/scheduled/:id/logs — Histórico de execuções
*/
const express_1 = require("express");
const zod_1 = require("zod");
const croner_1 = require("croner");
const prisma_1 = require("../../infra/database/prisma");
const recipientSchema = zod_1.z.object({
jid: zod_1.z.string(),
name: zod_1.z.string().optional(),
birthday: zod_1.z.string().optional(),
anniversary: zod_1.z.string().optional(),
signupDate: zod_1.z.string().optional(),
});
const scheduleSchema = zod_1.z.object({
instanceId: zod_1.z.string(),
name: zod_1.z.string().min(1).max(100),
templateId: zod_1.z.string().optional(),
payload: zod_1.z.record(zod_1.z.unknown()),
recipientMode: zod_1.z.enum(['MANUAL', 'ALL_CONTACTS', 'TAG_FILTER']).default('MANUAL'),
recipients: zod_1.z.array(recipientSchema).default([]),
scheduleType: zod_1.z.enum(['ONCE', 'RECURRING', 'EVENT']),
cronExpr: zod_1.z.string().optional(),
scheduledAt: zod_1.z.string().datetime().optional(),
eventType: zod_1.z.enum(['BIRTHDAY', 'ANNIVERSARY', 'SIGNUP_DAYS']).optional(),
timezone: zod_1.z.string().default('America/Sao_Paulo'),
});
function getId(req) {
const id = req.params['id'];
return Array.isArray(id) ? id[0] : id;
}
function calcNextRun(data) {
if (data.scheduleType === 'ONCE' && data.scheduledAt) {
return new Date(data.scheduledAt);
}
if (data.scheduleType === 'RECURRING' && data.cronExpr) {
try {
const job = new croner_1.Cron(data.cronExpr, { paused: true });
return job.nextRun() ?? null;
}
catch {
return null;
}
}
return null;
}
function buildScheduledRoutes(_scheduler) {
const router = (0, express_1.Router)();
// ── LIST ──────────────────────────────────────────────────────────────────
router.get('/', async (req, res) => {
const tenantId = req.tenantId;
const status = Array.isArray(req.query['status']) ? req.query['status'][0] : req.query['status'];
const type = Array.isArray(req.query['type']) ? req.query['type'][0] : req.query['type'];
const schedules = await prisma_1.prisma.scheduledMessage.findMany({
where: {
tenantId,
...(status ? { status: status } : {}),
...(type ? { scheduleType: type } : {}),
},
include: {
template: { select: { id: true, name: true, type: true } },
_count: { select: { logs: true } },
},
orderBy: { createdAt: 'desc' },
});
res.json(schedules);
});
// ── CREATE ────────────────────────────────────────────────────────────────
router.post('/', async (req, res) => {
const tenantId = req.tenantId;
const parse = scheduleSchema.safeParse(req.body);
if (!parse.success) {
res.status(400).json({ error: parse.error.errors });
return;
}
const data = parse.data;
const nextRunAt = calcNextRun(data);
const schedule = await prisma_1.prisma.scheduledMessage.create({
data: {
tenantId,
instanceId: data.instanceId,
name: data.name,
templateId: data.templateId,
payload: data.payload,
recipientMode: data.recipientMode,
recipients: data.recipients,
scheduleType: data.scheduleType,
cronExpr: data.cronExpr,
scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : null,
eventType: data.eventType ?? null,
timezone: data.timezone,
nextRunAt,
},
});
res.status(201).json(schedule);
});
// ── GET ONE ───────────────────────────────────────────────────────────────
router.get('/:id', async (req, res) => {
const tenantId = req.tenantId;
const sched = await prisma_1.prisma.scheduledMessage.findFirst({
where: { id: getId(req), tenantId },
include: {
template: true,
logs: { orderBy: { runAt: 'desc' }, take: 5 },
},
});
if (!sched) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
res.json(sched);
});
// ── UPDATE ────────────────────────────────────────────────────────────────
router.put('/:id', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const exists = await prisma_1.prisma.scheduledMessage.findFirst({
where: { id, tenantId },
});
if (!exists) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
const parse = scheduleSchema.partial().safeParse(req.body);
if (!parse.success) {
res.status(400).json({ error: parse.error.errors });
return;
}
const data = parse.data;
const nextRunAt = calcNextRun({ ...exists, ...data });
const { payload, recipients, scheduledAt, eventType, ...rest } = data;
const updated = await prisma_1.prisma.scheduledMessage.update({
where: { id },
data: {
...rest,
...(payload !== undefined ? { payload: payload } : {}),
...(recipients !== undefined ? { recipients: recipients } : {}),
...(scheduledAt !== undefined ? { scheduledAt: new Date(scheduledAt) } : {}),
...(eventType !== undefined ? { eventType } : {}),
status: 'ACTIVE',
nextRunAt,
},
});
res.json(updated);
});
// ── DELETE ────────────────────────────────────────────────────────────────
router.delete('/:id', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const exists = await prisma_1.prisma.scheduledMessage.findFirst({
where: { id, tenantId },
});
if (!exists) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
await prisma_1.prisma.scheduledMessage.delete({ where: { id } });
res.status(204).send();
});
// ── PAUSE ─────────────────────────────────────────────────────────────────
router.post('/:id/pause', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const exists = await prisma_1.prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
if (!exists) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
await prisma_1.prisma.scheduledMessage.update({ where: { id }, data: { status: 'PAUSED' } });
res.json({ ok: true });
});
// ── RESUME ────────────────────────────────────────────────────────────────
router.post('/:id/resume', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const exists = await prisma_1.prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
if (!exists) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
await prisma_1.prisma.scheduledMessage.update({ where: { id }, data: { status: 'ACTIVE' } });
res.json({ ok: true });
});
// ── RUN NOW ───────────────────────────────────────────────────────────────
router.post('/:id/run-now', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const sched = await prisma_1.prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
if (!sched) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
// Marca nextRunAt no passado — o tick do scheduler pega no próximo minuto
await prisma_1.prisma.scheduledMessage.update({
where: { id },
data: { nextRunAt: new Date(Date.now() - 1000), status: 'ACTIVE' },
});
res.json({ ok: true, message: 'Execução agendada para o próximo tick' });
});
// ── LOGS ──────────────────────────────────────────────────────────────────
router.get('/:id/logs', async (req, res) => {
const tenantId = req.tenantId;
const id = getId(req);
const exists = await prisma_1.prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
if (!exists) {
res.status(404).json({ error: 'Agendamento não encontrado' });
return;
}
const logs = await prisma_1.prisma.scheduledMessageLog.findMany({
where: { scheduleId: id },
orderBy: { runAt: 'desc' },
take: 50,
});
res.json(logs);
});
return router;
}