import { v } from "convex/values"; import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server"; import { internal, api } from "./_generated/api"; import { renderizarTemplate } from "./templatesMensagens"; // ========== INTERNAL QUERIES ========== /** * Obter email por ID (internal query) */ export const getEmailById = internalQuery({ args: { emailId: v.id("notificacoesEmail"), }, handler: async (ctx, args) => { return await ctx.db.get(args.emailId); }, }); /** * Obter configuração SMTP ativa (internal query) */ export const getActiveEmailConfig = internalQuery({ args: {}, handler: async (ctx) => { const config = await ctx.db .query("configuracaoEmail") .withIndex("by_ativo", (q) => q.eq("ativo", true)) .first(); return config; }, }); /** * Listar emails pendentes (internal query) */ export const listarEmailsPendentes = internalQuery({ args: { limite: v.optional(v.number()), }, handler: async (ctx, args) => { const emails = await ctx.db .query("notificacoesEmail") .withIndex("by_status", (q) => q.eq("status", "pendente")) .order("asc") // Mais antigos primeiro .take(args.limite || 10); return emails; }, }); // ========== INTERNAL MUTATIONS ========== /** * Marcar email como enviando (internal mutation) */ export const markEmailEnviando = internalMutation({ args: { emailId: v.id("notificacoesEmail"), }, handler: async (ctx, args) => { const email = await ctx.db.get(args.emailId); if (!email) return; await ctx.db.patch(args.emailId, { status: "enviando", ultimaTentativa: Date.now(), tentativas: email.tentativas + 1, }); }, }); /** * Marcar email como enviado (internal mutation) */ export const markEmailEnviado = internalMutation({ args: { emailId: v.id("notificacoesEmail"), }, handler: async (ctx, args) => { await ctx.db.patch(args.emailId, { status: "enviado", enviadoEm: Date.now(), }); }, }); /** * Marcar email como falha (internal mutation) */ export const markEmailFalha = internalMutation({ args: { emailId: v.id("notificacoesEmail"), erro: v.string(), }, handler: async (ctx, args) => { await ctx.db.patch(args.emailId, { status: "falha", erroDetalhes: args.erro, ultimaTentativa: Date.now(), }); }, }); // ========== PUBLIC MUTATIONS ========== /** * Enfileirar email para envio assíncrono */ export const enfileirarEmail = mutation({ args: { destinatario: v.string(), destinatarioId: v.optional(v.id("usuarios")), assunto: v.string(), corpo: v.string(), templateId: v.optional(v.id("templatesMensagens")), enviadoPor: v.id("usuarios"), // Obrigatório conforme schema }, handler: async (ctx, args) => { const emailId = await ctx.db.insert("notificacoesEmail", { destinatario: args.destinatario, destinatarioId: args.destinatarioId, assunto: args.assunto, corpo: args.corpo, templateId: args.templateId, status: "pendente", tentativas: 0, criadoEm: Date.now(), enviadoPor: args.enviadoPor, }); return emailId; }, }); /** * Enviar email usando template */ export const enviarEmailComTemplate = action({ args: { destinatario: v.string(), destinatarioId: v.optional(v.id("usuarios")), templateCodigo: v.string(), variaveis: v.optional(v.record(v.string(), v.string())), enviadoPor: v.id("usuarios"), // Obrigatório conforme schema }, handler: async (ctx, args) => { // Buscar template const template = await ctx.runQuery(api.templatesMensagens.obterTemplatePorCodigo, { codigo: args.templateCodigo, }); if (!template) { throw new Error(`Template não encontrado: ${args.templateCodigo}`); } // Renderizar template com variáveis const variaveisTemplate = args.variaveis || {}; const tituloRenderizado = renderizarTemplate(template.titulo, variaveisTemplate); const corpoRenderizado = renderizarTemplate(template.corpo, variaveisTemplate); // Enfileirar email const emailId = await ctx.runMutation(api.email.enfileirarEmail, { destinatario: args.destinatario, destinatarioId: args.destinatarioId, assunto: tituloRenderizado, corpo: corpoRenderizado, templateId: template._id, enviadoPor: args.enviadoPor, }); return emailId; }, }); // ========== INTERNAL MUTATION (CRON) ========== /** * Processar fila de emails pendentes (chamado pelo cron) */ export const processarFilaEmails = internalMutation({ args: {}, handler: async (ctx) => { // Buscar emails pendentes (limitado a 10 por vez para não sobrecarregar) const emailsPendentes = await ctx.db .query("notificacoesEmail") .filter((q) => q.eq(q.field("status"), "pendente")) .order("asc") // Mais antigos primeiro .take(10); if (emailsPendentes.length === 0) { return { processados: 0 }; } // Agendar envio de cada email via action for (const email of emailsPendentes) { // Agendar envio assíncrono (não bloqueia o cron) ctx.scheduler.runAfter(0, api.actions.email.enviar, { emailId: email._id, }).catch((error) => { console.error(`Erro ao agendar envio de email ${email._id}:`, error); }); } return { processados: emailsPendentes.length }; }, }); // ========== QUERIES ========== /** * Listar emails da fila (para monitoramento) */ export const listarFilaEmails = query({ args: { limite: v.optional(v.number()), status: v.optional(v.union( v.literal("pendente"), v.literal("enviando"), v.literal("enviado"), v.literal("falha") )), }, handler: async (ctx, args) => { let emails; // Filtrar por status se fornecido if (args.status) { emails = await ctx.db .query("notificacoesEmail") .withIndex("by_status", (q) => q.eq("status", args.status!)) .order("desc") .take(args.limite || 50); } else { // Sem filtro, buscar todos e ordenar por data de criação const todosEmails = await ctx.db.query("notificacoesEmail").collect(); todosEmails.sort((a, b) => b.criadoEm - a.criadoEm); emails = todosEmails.slice(0, args.limite || 50); } return emails; }, }); /** * Obter estatísticas da fila de emails (para debug e monitoramento) */ export const obterEstatisticasFilaEmails = query({ args: {}, returns: v.object({ pendentes: v.number(), enviando: v.number(), enviados: v.number(), falhas: v.number(), total: v.number(), }), handler: async (ctx) => { const todosEmails = await ctx.db.query("notificacoesEmail").collect(); const estatisticas = { pendentes: 0, enviando: 0, enviados: 0, falhas: 0, total: todosEmails.length, }; for (const email of todosEmails) { switch (email.status) { case "pendente": estatisticas.pendentes++; break; case "enviando": estatisticas.enviando++; break; case "enviado": estatisticas.enviados++; break; case "falha": estatisticas.falhas++; break; } } return estatisticas; }, }); // ========== PUBLIC MUTATIONS (MANUAL) ========== /** * Processar fila de emails manualmente (para uso em interface) */ export const processarFilaEmailsManual = action({ args: { limite: v.optional(v.number()), }, returns: v.object({ sucesso: v.boolean(), processados: v.number(), falhas: v.number(), erro: v.optional(v.string()), }), handler: async (ctx, args) => { try { // Buscar emails pendentes const emailsPendentes = await ctx.runQuery(internal.email.listarEmailsPendentes, { limite: args.limite || 10, }); if (emailsPendentes.length === 0) { return { sucesso: true, processados: 0, falhas: 0 }; } let processados = 0; let falhas = 0; // Processar cada email for (const email of emailsPendentes) { try { // Agendar envio via action await ctx.scheduler.runAfter(0, api.actions.email.enviar, { emailId: email._id, }); processados++; } catch (error) { console.error(`Erro ao agendar envio de email ${email._id}:`, error); falhas++; } } return { sucesso: true, processados, falhas }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { sucesso: false, processados: 0, falhas: 0, erro: errorMessage, }; } }, });