244 lines
6.6 KiB
TypeScript
244 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", { status: 200, 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);
|
|
}
|
|
});
|
|
|