Changes
This commit is contained in:
@@ -26,3 +26,9 @@ verify_jwt = true
|
||||
|
||||
[functions.send-consultation-reminder]
|
||||
verify_jwt = false
|
||||
|
||||
[functions.send-notification]
|
||||
verify_jwt = false
|
||||
|
||||
[functions.daily-reminders]
|
||||
verify_jwt = false
|
||||
|
||||
138
supabase/functions/daily-reminders/index.ts
Normal file
138
supabase/functions/daily-reminders/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
interface ConsultingSlot {
|
||||
id: string;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
topic_category: string;
|
||||
meet_link: string | null;
|
||||
user_id: string;
|
||||
profiles: {
|
||||
full_name: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Get tomorrow's date
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
console.log(`Checking consultations for: ${tomorrowStr}`);
|
||||
|
||||
// Get confirmed consulting slots for tomorrow
|
||||
const { data: slots, error } = await supabase
|
||||
.from("consulting_slots")
|
||||
.select(`
|
||||
id,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
topic_category,
|
||||
meet_link,
|
||||
user_id,
|
||||
profiles:user_id (full_name, email)
|
||||
`)
|
||||
.eq("date", tomorrowStr)
|
||||
.eq("status", "confirmed");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching slots:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Found ${slots?.length || 0} consultations for tomorrow`);
|
||||
|
||||
const results: { email: string; success: boolean; error?: string }[] = [];
|
||||
|
||||
// Send reminder to each client
|
||||
for (const slot of (slots || []) as any[]) {
|
||||
try {
|
||||
const profile = slot.profiles;
|
||||
if (!profile?.email) continue;
|
||||
|
||||
// Call send-notification function
|
||||
const { error: notifyError } = await supabase.functions.invoke("send-notification", {
|
||||
body: {
|
||||
template_key: "consultation_reminder",
|
||||
recipient_email: profile.email,
|
||||
recipient_name: profile.full_name,
|
||||
variables: {
|
||||
consultation_date: new Date(slot.date).toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}),
|
||||
consultation_time: `${slot.start_time.substring(0, 5)} - ${slot.end_time.substring(0, 5)}`,
|
||||
topic_category: slot.topic_category,
|
||||
meet_link: slot.meet_link || "Link akan dikirim segera",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
results.push({
|
||||
email: profile.email,
|
||||
success: !notifyError,
|
||||
error: notifyError?.message,
|
||||
});
|
||||
|
||||
console.log(`Reminder sent to ${profile.email}: ${notifyError ? 'FAILED' : 'SUCCESS'}`);
|
||||
} catch (err: any) {
|
||||
results.push({
|
||||
email: slot.profiles?.email || "unknown",
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get platform settings for admin digest
|
||||
const { data: settings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("smtp_from_email")
|
||||
.single();
|
||||
|
||||
// Send digest to admin if there are consultations
|
||||
if (slots && slots.length > 0 && settings?.smtp_from_email) {
|
||||
const slotsList = (slots as any[])
|
||||
.map(s => `- ${s.start_time?.substring(0, 5)}: ${s.profiles?.full_name || 'N/A'} (${s.topic_category})`)
|
||||
.join('\n');
|
||||
|
||||
console.log(`Admin digest: ${slots.length} consultations tomorrow`);
|
||||
// Could send admin digest here
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: `Processed ${results.length} reminders`,
|
||||
results
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error in daily reminders:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
327
supabase/functions/send-notification/index.ts
Normal file
327
supabase/functions/send-notification/index.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
interface NotificationRequest {
|
||||
template_key: string;
|
||||
recipient_email: string;
|
||||
recipient_name?: string;
|
||||
variables?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface SMTPConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
from_name: string;
|
||||
from_email: string;
|
||||
use_tls: boolean;
|
||||
}
|
||||
|
||||
interface EmailPayload {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
from_name: string;
|
||||
from_email: string;
|
||||
}
|
||||
|
||||
// Send via SMTP
|
||||
async function sendViaSMTP(payload: EmailPayload, config: SMTPConfig): Promise<void> {
|
||||
const boundary = "----=_Part_" + Math.random().toString(36).substr(2, 9);
|
||||
const emailContent = [
|
||||
`From: "${payload.from_name}" <${payload.from_email}>`,
|
||||
`To: ${payload.to}`,
|
||||
`Subject: =?UTF-8?B?${btoa(payload.subject)}?=`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
||||
``,
|
||||
`--${boundary}`,
|
||||
`Content-Type: text/html; charset=UTF-8`,
|
||||
``,
|
||||
payload.html,
|
||||
`--${boundary}--`,
|
||||
].join("\r\n");
|
||||
|
||||
const conn = config.use_tls
|
||||
? await Deno.connectTls({ hostname: config.host, port: config.port })
|
||||
: await Deno.connect({ hostname: config.host, port: config.port });
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
async function readResponse(): Promise<string> {
|
||||
const buffer = new Uint8Array(1024);
|
||||
const n = await conn.read(buffer);
|
||||
if (n === null) return "";
|
||||
return decoder.decode(buffer.subarray(0, n));
|
||||
}
|
||||
|
||||
async function sendCommand(cmd: string): Promise<string> {
|
||||
await conn.write(encoder.encode(cmd + "\r\n"));
|
||||
return await readResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
await readResponse();
|
||||
await sendCommand(`EHLO localhost`);
|
||||
await sendCommand("AUTH LOGIN");
|
||||
await sendCommand(btoa(config.username));
|
||||
const authResponse = await sendCommand(btoa(config.password));
|
||||
|
||||
if (!authResponse.includes("235") && !authResponse.includes("Authentication successful")) {
|
||||
throw new Error("SMTP Authentication failed");
|
||||
}
|
||||
|
||||
await sendCommand(`MAIL FROM:<${payload.from_email}>`);
|
||||
await sendCommand(`RCPT TO:<${payload.to}>`);
|
||||
await sendCommand("DATA");
|
||||
await conn.write(encoder.encode(emailContent + "\r\n.\r\n"));
|
||||
await readResponse();
|
||||
await sendCommand("QUIT");
|
||||
conn.close();
|
||||
} catch (error) {
|
||||
conn.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Send via Resend
|
||||
async function sendViaResend(payload: EmailPayload, apiKey: string): Promise<void> {
|
||||
const response = await fetch("https://api.resend.com/emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: `${payload.from_name} <${payload.from_email}>`,
|
||||
to: [payload.to],
|
||||
subject: payload.subject,
|
||||
html: payload.html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Resend error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Send via ElasticEmail
|
||||
async function sendViaElasticEmail(payload: EmailPayload, apiKey: string): Promise<void> {
|
||||
const response = await fetch("https://api.elasticemail.com/v4/emails/transactional", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-ElasticEmail-ApiKey": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Recipients: {
|
||||
To: [payload.to],
|
||||
},
|
||||
Content: {
|
||||
From: `${payload.from_name} <${payload.from_email}>`,
|
||||
Subject: payload.subject,
|
||||
Body: [
|
||||
{
|
||||
ContentType: "HTML",
|
||||
Content: payload.html,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`ElasticEmail error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Send via SendGrid
|
||||
async function sendViaSendGrid(payload: EmailPayload, apiKey: string): Promise<void> {
|
||||
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
personalizations: [{ to: [{ email: payload.to }] }],
|
||||
from: { email: payload.from_email, name: payload.from_name },
|
||||
subject: payload.subject,
|
||||
content: [{ type: "text/html", value: payload.html }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`SendGrid error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Send via Mailgun
|
||||
async function sendViaMailgun(payload: EmailPayload, apiKey: string, domain: string): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append("from", `${payload.from_name} <${payload.from_email}>`);
|
||||
formData.append("to", payload.to);
|
||||
formData.append("subject", payload.subject);
|
||||
formData.append("html", payload.html);
|
||||
|
||||
const response = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Basic ${btoa(`api:${apiKey}`)}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Mailgun error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceVariables(template: string, variables: Record<string, string>): string {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
const body: NotificationRequest = await req.json();
|
||||
const { template_key, recipient_email, recipient_name, variables = {} } = body;
|
||||
|
||||
// Get notification template
|
||||
const { data: template, error: templateError } = await supabase
|
||||
.from("notification_templates")
|
||||
.select("*")
|
||||
.eq("template_key", template_key)
|
||||
.eq("is_active", true)
|
||||
.single();
|
||||
|
||||
if (templateError || !template) {
|
||||
console.log(`Template not found: ${template_key}`);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Template not found or inactive" }),
|
||||
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get platform settings
|
||||
const { data: settings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (!settings) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Platform settings not configured" }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Build email payload
|
||||
const allVariables = {
|
||||
recipient_name: recipient_name || "Pelanggan",
|
||||
platform_name: settings.brand_name || "Platform",
|
||||
...variables,
|
||||
};
|
||||
|
||||
const subject = replaceVariables(template.subject, allVariables);
|
||||
const htmlBody = replaceVariables(template.body_html || template.body_text || "", allVariables);
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to: recipient_email,
|
||||
subject,
|
||||
html: htmlBody,
|
||||
from_name: settings.brand_email_from_name || settings.brand_name || "Notifikasi",
|
||||
from_email: settings.smtp_from_email || "noreply@example.com",
|
||||
};
|
||||
|
||||
// Determine provider and send
|
||||
const provider = settings.integration_email_provider || "smtp";
|
||||
console.log(`Sending email via ${provider} to ${recipient_email}`);
|
||||
|
||||
switch (provider) {
|
||||
case "smtp":
|
||||
await sendViaSMTP(emailPayload, {
|
||||
host: settings.smtp_host,
|
||||
port: settings.smtp_port || 587,
|
||||
username: settings.smtp_username,
|
||||
password: settings.smtp_password,
|
||||
from_name: emailPayload.from_name,
|
||||
from_email: emailPayload.from_email,
|
||||
use_tls: settings.smtp_use_tls ?? true,
|
||||
});
|
||||
break;
|
||||
|
||||
case "resend":
|
||||
const resendKey = Deno.env.get("RESEND_API_KEY");
|
||||
if (!resendKey) throw new Error("RESEND_API_KEY not configured");
|
||||
await sendViaResend(emailPayload, resendKey);
|
||||
break;
|
||||
|
||||
case "elasticemail":
|
||||
const elasticKey = Deno.env.get("ELASTICEMAIL_API_KEY");
|
||||
if (!elasticKey) throw new Error("ELASTICEMAIL_API_KEY not configured");
|
||||
await sendViaElasticEmail(emailPayload, elasticKey);
|
||||
break;
|
||||
|
||||
case "sendgrid":
|
||||
const sendgridKey = Deno.env.get("SENDGRID_API_KEY");
|
||||
if (!sendgridKey) throw new Error("SENDGRID_API_KEY not configured");
|
||||
await sendViaSendGrid(emailPayload, sendgridKey);
|
||||
break;
|
||||
|
||||
case "mailgun":
|
||||
const mailgunKey = Deno.env.get("MAILGUN_API_KEY");
|
||||
const mailgunDomain = Deno.env.get("MAILGUN_DOMAIN");
|
||||
if (!mailgunKey || !mailgunDomain) throw new Error("MAILGUN credentials not configured");
|
||||
await sendViaMailgun(emailPayload, mailgunKey, mailgunDomain);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown email provider: ${provider}`);
|
||||
}
|
||||
|
||||
// Log notification
|
||||
await supabase.from("notification_logs").insert({
|
||||
template_id: template.id,
|
||||
recipient_email,
|
||||
channel: "email",
|
||||
status: "sent",
|
||||
sent_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log(`Email sent successfully to ${recipient_email}`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "Notification sent" }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error sending notification:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user