Files
meet-hub/supabase/functions/send-notification/index.ts
dwindown 4f9a6f4ae3 Fix variable replacement format in send-notification
The replaceVariables function was only supporting {{key}} format but
the email templates use {key} format (single braces). Updated to support
both formats for compatibility.

Changes:
- Added support for {key} format in addition to {{key}}
- Ensures all template variables are properly replaced

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-03 09:10:19 +07:00

393 lines
12 KiB
TypeScript

import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { EmailTemplateRenderer } from "../shared/email-template-renderer.ts";
import QRCode from 'https://esm.sh/qrcode@1.5.3';
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 Mailketing API
async function sendViaMailketing(payload: EmailPayload, apiToken: string): Promise<void> {
const params = new URLSearchParams();
params.append('api_token', apiToken);
params.append('from_name', payload.from_name);
params.append('from_email', payload.from_email);
params.append('recipient', payload.to);
params.append('subject', payload.subject);
params.append('content', payload.html);
console.log(`Sending email via Mailketing to ${payload.to}`);
const response = await fetch('https://api.mailketing.co.id/api/v1/send', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
});
if (!response.ok) {
const errorText = await response.text();
console.error('Mailketing API error:', response.status, errorText);
throw new Error(`Mailketing API error: ${response.status} ${errorText}`);
}
const result = await response.json();
console.log('Mailketing API response:', result);
}
// 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)) {
// Support both {key} and {{key}} formats
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
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("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,
};
// Special handling for order_created: generate QR code image
if (template_key === 'order_created' && allVariables.qr_string) {
console.log('[SEND-NOTIFICATION] Generating QR code for order_created email');
try {
const qrDataUrl = await QRCode.toDataURL(allVariables.qr_string, {
width: 300,
margin: 2,
color: { dark: '#000000', light: '#FFFFFF' }
});
allVariables.qr_code_image = qrDataUrl;
console.log('[SEND-NOTIFICATION] QR code generated successfully');
} catch (qrError) {
console.error('[SEND-NOTIFICATION] Failed to generate QR code:', qrError);
// Continue without QR code - don't fail the email
allVariables.qr_code_image = '';
}
}
const subject = replaceVariables(template.email_subject || template.subject || "", allVariables);
const htmlContent = replaceVariables(template.email_body_html || template.body_html || template.body_text || "", allVariables);
// Wrap with master template for consistent branding
const htmlBody = EmailTemplateRenderer.render({
subject: subject,
content: htmlContent,
brandName: settings.brand_name || "ACCESS HUB",
});
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 || "mailketing";
console.log(`Sending email via ${provider} to ${recipient_email}`);
switch (provider) {
case "mailketing":
const mailketingToken = settings.mailketing_api_token || settings.api_token;
if (!mailketingToken) throw new Error("Mailketing API token not configured");
await sendViaMailketing(emailPayload, mailketingToken);
break;
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" } }
);
}
});