243 lines
6.6 KiB
TypeScript

import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { mydb } from "../../lib/mySupabase.ts";
import { corsHeaders, jsonResponse, errorResponse } from "../../lib/utils.ts";
/**
* POST /notifications-worker
* Worker que processa fila de notificações pendentes
* Deve ser executado via cron job a cada 1-5 minutos
*
* Processa:
* - SMS via Twilio
* - Email via SendGrid/Resend
* - WhatsApp via Twilio
* - Push notifications
*/
const TWILIO_SID = Deno.env.get("TWILIO_SID");
const TWILIO_AUTH_TOKEN = Deno.env.get("TWILIO_AUTH_TOKEN");
const TWILIO_FROM = Deno.env.get("TWILIO_FROM");
async function sendSMS(to: string, message: string) {
if (!TWILIO_SID || !TWILIO_AUTH_TOKEN || !TWILIO_FROM) {
console.warn("[SMS] Twilio não configurado");
return { success: false, error: "Twilio not configured" };
}
const url = `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`;
const auth = btoa(`${TWILIO_SID}:${TWILIO_AUTH_TOKEN}`);
try {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Basic ${auth}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
To: to,
From: TWILIO_FROM!,
Body: message,
}),
});
const data = await res.json();
if (!res.ok) {
return { success: false, error: data.message || "SMS failed" };
}
return { success: true, sid: data.sid };
} catch (error) {
console.error("[SMS] Error:", error);
return { success: false, error: String(error) };
}
}
async function sendEmail(to: string, subject: string, body: string) {
// TODO: Implementar SendGrid ou Resend
console.log("[Email] Would send to:", to, subject);
return { success: true, message: "Email sending not implemented yet" };
}
async function sendWhatsApp(to: string, message: string) {
if (!TWILIO_SID || !TWILIO_AUTH_TOKEN) {
return { success: false, error: "Twilio not configured" };
}
const url = `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`;
const auth = btoa(`${TWILIO_SID}:${TWILIO_AUTH_TOKEN}`);
try {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Basic ${auth}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
To: `whatsapp:${to}`,
From: `whatsapp:${TWILIO_FROM}`,
Body: message,
}),
});
const data = await res.json();
if (!res.ok) {
return { success: false, error: data.message || "WhatsApp failed" };
}
return { success: true, sid: data.sid };
} catch (error) {
return { success: false, error: String(error) };
}
}
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders() });
}
try {
console.log("[Worker] Iniciando processamento de notificações...");
// 1. Buscar notificações pendentes ou agendadas para agora
const now = new Date().toISOString();
const { data: notifications, error: fetchError } = await mydb
.from("notifications_queue")
.select("*")
.eq("status", "pending")
.or(`scheduled_at.is.null,scheduled_at.lte.${now}`)
.lt("attempts", 3)
.order("created_at", { ascending: true })
.limit(50); // Processa 50 por vez
if (fetchError) {
console.error("[Worker] Erro ao buscar notificações:", fetchError);
return errorResponse(fetchError.message, 500);
}
if (!notifications || notifications.length === 0) {
console.log("[Worker] Nenhuma notificação pendente");
return jsonResponse({
success: true,
processed: 0,
message: "No pending notifications",
});
}
console.log(`[Worker] Processando ${notifications.length} notificações...`);
let processed = 0;
let failed = 0;
const results = [];
for (const notification of notifications) {
const { id, type, payload, attempts } = notification;
// Marcar como processando
await mydb
.from("notifications_queue")
.update({ status: "processing" })
.eq("id", id);
let result;
let success = false;
try {
switch (type) {
case "sms":
result = await sendSMS(payload.to, payload.message);
success = result.success;
break;
case "email":
result = await sendEmail(payload.to, payload.subject, payload.body);
success = result.success;
break;
case "whatsapp":
result = await sendWhatsApp(payload.to, payload.message);
success = result.success;
break;
case "push":
// TODO: Implementar push notifications (Firebase, OneSignal, etc)
result = { success: false, error: "Push not implemented" };
break;
default:
result = { success: false, error: "Unknown notification type" };
}
if (success) {
// Marcar como enviada
await mydb
.from("notifications_queue")
.update({
status: "sent",
sent_at: new Date().toISOString(),
attempts: attempts + 1,
})
.eq("id", id);
processed++;
} else {
// Incrementar tentativas e marcar erro
const newAttempts = attempts + 1;
const finalStatus = newAttempts >= 3 ? "failed" : "pending";
await mydb
.from("notifications_queue")
.update({
status: finalStatus,
attempts: newAttempts,
last_error: result.error || "Unknown error",
})
.eq("id", id);
failed++;
}
results.push({
id,
type,
success,
error: result.error || null,
});
} catch (error) {
console.error(`[Worker] Erro ao processar notificação ${id}:`, error);
await mydb
.from("notifications_queue")
.update({
status: "pending",
attempts: attempts + 1,
last_error: String(error),
})
.eq("id", id);
failed++;
}
}
console.log(
`[Worker] Concluído: ${processed} enviadas, ${failed} falharam`
);
return jsonResponse({
success: true,
processed,
failed,
total: notifications.length,
results,
});
} catch (error: unknown) {
console.error("[Worker] Erro geral:", error);
const err = error as Error;
return errorResponse(err.message, 500);
}
});