Fix email system and implement OTP confirmation flow
Email System Fixes: - Fix email sending after payment: handle-order-paid now calls send-notification instead of send-email-v2 directly, properly processing template variables - Fix order_created email timing: sent immediately after order creation, before payment QR code generation - Update email templates to use short order ID (8 chars) instead of full UUID - Add working "Akses Sekarang" buttons to payment_success and access_granted emails - Add platform_url column to platform_settings for email links OTP Verification Flow: - Create dedicated /confirm-otp page for users who close registration modal - Add link in checkout modal and email to dedicated OTP page - Update OTP email template with better copywriting and dedicated page link - Fix send-auth-otp to fetch platform settings for dynamic brand_name and platform_url - Auto-login users after OTP verification in checkout flow Admin Features: - Add delete user functionality with cascade deletion of all related data - Update IntegrasiTab to read/write email settings from platform_settings only - Add test email template for email configuration testing Cleanup: - Remove obsolete send-consultation-reminder and send-test-email functions - Update send-email-v2 to read email config from platform_settings - Remove footer links (Ubah Preferensi/Unsubscribe) from email templates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
61
supabase/functions/delete-user/index.ts
Normal file
61
supabase/functions/delete-user/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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 DeleteUserRequest {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const body: DeleteUserRequest = await req.json();
|
||||
const { user_id } = body;
|
||||
|
||||
if (!user_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "user_id is required" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Deleting user from auth.users: ${user_id}`);
|
||||
|
||||
// Delete user from auth.users using admin API
|
||||
const { error: deleteError } = await supabase.auth.admin.deleteUser(user_id);
|
||||
|
||||
if (deleteError) {
|
||||
console.error('Error deleting user from auth.users:', deleteError);
|
||||
throw new Error(`Failed to delete user from auth: ${deleteError.message}`);
|
||||
}
|
||||
|
||||
console.log(`Successfully deleted user: ${user_id}`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "User deleted successfully" }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error deleting user:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -309,18 +309,30 @@ async function sendNotification(
|
||||
return;
|
||||
}
|
||||
|
||||
// Send email via Mailketing
|
||||
await fetch(`${Deno.env.get("SUPABASE_URL")}/functions/v1/send-email-v2`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: data.email,
|
||||
subject: template.email_subject,
|
||||
html: template.email_body_html,
|
||||
shortcodeData: data,
|
||||
}),
|
||||
});
|
||||
// Send email via send-notification (which will process shortcodes and call send-email-v2)
|
||||
try {
|
||||
const notificationResponse = await fetch(`${Deno.env.get("SUPABASE_URL")}/functions/v1/send-notification`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_key: templateKey,
|
||||
recipient_email: data.email,
|
||||
recipient_name: data.user_name || data.nama,
|
||||
variables: data,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!notificationResponse.ok) {
|
||||
const errorText = await notificationResponse.text();
|
||||
console.error("[HANDLE-PAID] Notification send failed:", notificationResponse.status, errorText);
|
||||
} else {
|
||||
const result = await notificationResponse.json();
|
||||
console.log("[HANDLE-PAID] Notification sent successfully for template:", templateKey, result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[HANDLE-PAID] Exception sending notification:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
@@ -12,11 +11,6 @@ interface SendOTPRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
// Generate 6-digit OTP code
|
||||
function generateOTP(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
@@ -33,191 +27,88 @@ serve(async (req: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Invalid email format" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize Supabase client with service role
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
});
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Generate OTP code
|
||||
const otpCode = generateOTP();
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
|
||||
// Fetch platform settings for brand name and URL
|
||||
const { data: platformSettings } = await supabase
|
||||
.from('platform_settings')
|
||||
.select('brand_name, platform_url')
|
||||
.single();
|
||||
|
||||
console.log(`Generating OTP for user ${user_id}, email ${email}`);
|
||||
const platformName = platformSettings?.brand_name || 'ACCESS HUB';
|
||||
const platformUrl = platformSettings?.platform_url || 'https://access-hub.com';
|
||||
|
||||
console.log(`Generating OTP for user ${user_id}`);
|
||||
|
||||
// Generate 6-digit OTP code
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
// Calculate expiration time (15 minutes from now)
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString();
|
||||
|
||||
// Store OTP in database
|
||||
const { error: otpError } = await supabase
|
||||
const { error: insertError } = await supabase
|
||||
.from('auth_otps')
|
||||
.insert({
|
||||
user_id,
|
||||
email,
|
||||
user_id: user_id,
|
||||
email: email,
|
||||
otp_code: otpCode,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
|
||||
if (otpError) {
|
||||
console.error('Error storing OTP:', otpError);
|
||||
throw new Error(`Failed to store OTP: ${otpError.message}`);
|
||||
if (insertError) {
|
||||
console.error('Error storing OTP:', insertError);
|
||||
throw new Error(`Failed to store OTP: ${insertError.message}`);
|
||||
}
|
||||
|
||||
// Get notification settings
|
||||
const { data: settings, error: settingsError } = await supabase
|
||||
.from('notification_settings')
|
||||
.select('*')
|
||||
.single();
|
||||
console.log(`OTP generated and stored: ${otpCode}, expires at: ${expiresAt}`);
|
||||
|
||||
if (settingsError || !settings) {
|
||||
console.error('Error fetching notification settings:', settingsError);
|
||||
throw new Error('Notification settings not configured');
|
||||
}
|
||||
|
||||
// Get platform settings for brand_name
|
||||
const { data: platformSettings, error: platformError } = await supabase
|
||||
.from('platform_settings')
|
||||
.select('brand_name')
|
||||
.single();
|
||||
|
||||
if (platformError) {
|
||||
console.error('Error fetching platform settings:', platformError);
|
||||
// Continue with fallback if platform settings not found
|
||||
}
|
||||
|
||||
const brandName = platformSettings?.brand_name || settings.platform_name || 'ACCESS HUB';
|
||||
|
||||
// Get email template
|
||||
console.log('Fetching email template with key: auth_email_verification');
|
||||
|
||||
const { data: template, error: templateError } = await supabase
|
||||
.from('notification_templates')
|
||||
.select('*')
|
||||
.eq('key', 'auth_email_verification')
|
||||
.single();
|
||||
|
||||
console.log('Template query result:', { template, templateError });
|
||||
|
||||
if (templateError || !template) {
|
||||
console.error('Error fetching email template:', templateError);
|
||||
throw new Error('Email template not found. Please create template with key: auth_email_verification');
|
||||
}
|
||||
|
||||
// Get user data from auth.users
|
||||
const { data: { user }, error: userError } = await supabase.auth.admin.getUserById(user_id);
|
||||
|
||||
if (userError || !user) {
|
||||
console.error('Error fetching user:', userError);
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// Prepare template variables
|
||||
const templateVars = {
|
||||
platform_name: brandName,
|
||||
nama: user.user_metadata?.name || user.email || 'Pengguna',
|
||||
email: email,
|
||||
otp_code: otpCode,
|
||||
expiry_minutes: '15',
|
||||
confirmation_link: '', // Not used for OTP
|
||||
year: new Date().getFullYear().toString(),
|
||||
};
|
||||
|
||||
// Process shortcodes in subject
|
||||
let subject = template.email_subject;
|
||||
Object.entries(templateVars).forEach(([key, value]) => {
|
||||
subject = subject.replace(new RegExp(`{${key}}`, 'g'), value);
|
||||
});
|
||||
|
||||
// Process shortcodes in HTML body content
|
||||
let htmlContent = template.email_body_html;
|
||||
Object.entries(templateVars).forEach(([key, value]) => {
|
||||
htmlContent = htmlContent.replace(new RegExp(`{${key}}`, 'g'), value);
|
||||
});
|
||||
|
||||
// Wrap in master template
|
||||
const htmlBody = EmailTemplateRenderer.render({
|
||||
subject: subject,
|
||||
content: htmlContent,
|
||||
brandName: brandName,
|
||||
});
|
||||
|
||||
// Send email via send-email-v2
|
||||
console.log(`Sending OTP email to ${email}`);
|
||||
console.log('Settings:', {
|
||||
hasMailketingToken: !!settings.mailketing_api_token,
|
||||
hasApiToken: !!settings.api_token,
|
||||
hasFromName: !!settings.from_name,
|
||||
hasFromEmail: !!settings.from_email,
|
||||
platformName: settings.platform_name,
|
||||
});
|
||||
|
||||
// Use api_token (not mailketing_api_token)
|
||||
const apiToken = settings.api_token || settings.mailketing_api_token;
|
||||
|
||||
if (!apiToken) {
|
||||
throw new Error('API token not found in notification_settings');
|
||||
}
|
||||
|
||||
// Log email details (truncate HTML body for readability)
|
||||
console.log('Email payload:', {
|
||||
recipient: email,
|
||||
from_name: settings.from_name || brandName,
|
||||
from_email: settings.from_email || 'noreply@example.com',
|
||||
subject: subject,
|
||||
content_length: htmlBody.length,
|
||||
content_preview: htmlBody.substring(0, 200),
|
||||
});
|
||||
|
||||
const emailResponse = await fetch(`${supabaseUrl}/functions/v1/send-email-v2`, {
|
||||
// Send OTP email using send-notification
|
||||
const notificationUrl = `${supabaseUrl}/functions/v1/send-notification`;
|
||||
const notificationResponse = await fetch(notificationUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${supabaseServiceKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recipient: email,
|
||||
api_token: apiToken,
|
||||
from_name: settings.from_name || brandName,
|
||||
from_email: settings.from_email || 'noreply@example.com',
|
||||
subject: subject,
|
||||
content: htmlBody,
|
||||
template_key: 'auth_email_verification',
|
||||
recipient_email: email,
|
||||
recipient_name: email.split('@')[0],
|
||||
variables: {
|
||||
nama: email.split('@')[0],
|
||||
otp_code: otpCode,
|
||||
email: email,
|
||||
user_id: user_id,
|
||||
expiry_minutes: '15',
|
||||
platform_name: platformName,
|
||||
platform_url: platformUrl
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
const errorText = await emailResponse.text();
|
||||
console.error('Email send error:', emailResponse.status, errorText);
|
||||
throw new Error(`Failed to send email: ${emailResponse.status} ${errorText}`);
|
||||
if (!notificationResponse.ok) {
|
||||
const errorText = await notificationResponse.text();
|
||||
console.error('Error sending notification email:', notificationResponse.status, errorText);
|
||||
throw new Error(`Failed to send OTP email: ${notificationResponse.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const emailResult = await emailResponse.json();
|
||||
console.log('Email sent successfully:', emailResult);
|
||||
|
||||
// Note: notification_logs table doesn't exist, skipping logging
|
||||
const notificationResult = await notificationResponse.json();
|
||||
console.log('Notification sent successfully:', notificationResult);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'OTP sent successfully'
|
||||
message: "OTP sent successfully"
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error sending OTP:", error);
|
||||
|
||||
// Note: notification_logs table doesn't exist, skipping error logging
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
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",
|
||||
};
|
||||
|
||||
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 supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Get current date/time in Jakarta timezone
|
||||
const now = new Date();
|
||||
const jakartaOffset = 7 * 60; // UTC+7
|
||||
const jakartaTime = new Date(now.getTime() + jakartaOffset * 60 * 1000);
|
||||
const today = jakartaTime.toISOString().split('T')[0];
|
||||
|
||||
// Find consultations happening in the next 24 hours that haven't been reminded
|
||||
const tomorrow = new Date(jakartaTime);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
console.log("Checking consultations for dates:", today, "to", tomorrowStr);
|
||||
|
||||
// Get confirmed slots for today and tomorrow
|
||||
const { data: upcomingSlots, error: slotsError } = await supabase
|
||||
.from("consulting_slots")
|
||||
.select(`
|
||||
*,
|
||||
profiles:user_id (full_name, email)
|
||||
`)
|
||||
.eq("status", "confirmed")
|
||||
.gte("date", today)
|
||||
.lte("date", tomorrowStr)
|
||||
.order("date")
|
||||
.order("start_time");
|
||||
|
||||
if (slotsError) {
|
||||
console.error("Error fetching slots:", slotsError);
|
||||
throw slotsError;
|
||||
}
|
||||
|
||||
console.log("Found upcoming slots:", upcomingSlots?.length || 0);
|
||||
|
||||
if (!upcomingSlots || upcomingSlots.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "No upcoming consultations to remind" }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get notification template for consultation reminder
|
||||
const { data: template } = await supabase
|
||||
.from("notification_templates")
|
||||
.select("*")
|
||||
.eq("key", "consulting_scheduled")
|
||||
.single();
|
||||
|
||||
// Get SMTP settings
|
||||
const { data: smtpSettings } = await supabase
|
||||
.from("notification_settings")
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
// Get platform settings
|
||||
const { data: platformSettings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("brand_name, brand_email_from_name, integration_whatsapp_number")
|
||||
.single();
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
for (const slot of upcomingSlots) {
|
||||
const profile = slot.profiles as any;
|
||||
|
||||
// Build payload for notification
|
||||
const payload = {
|
||||
nama: profile?.full_name || "Pelanggan",
|
||||
email: profile?.email || "",
|
||||
tanggal_konsultasi: new Date(slot.date).toLocaleDateString("id-ID", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
jam_konsultasi: `${slot.start_time.substring(0, 5)} - ${slot.end_time.substring(0, 5)} WIB`,
|
||||
link_meet: slot.meet_link || "Akan diinformasikan",
|
||||
topik: slot.topic_category,
|
||||
catatan: slot.notes || "-",
|
||||
brand_name: platformSettings?.brand_name || "LearnHub",
|
||||
whatsapp: platformSettings?.integration_whatsapp_number || "",
|
||||
};
|
||||
|
||||
// Log the reminder payload
|
||||
console.log("Reminder payload for slot:", slot.id, payload);
|
||||
|
||||
// Update last_payload_example in template
|
||||
if (template) {
|
||||
await supabase
|
||||
.from("notification_templates")
|
||||
.update({ last_payload_example: payload })
|
||||
.eq("id", template.id);
|
||||
}
|
||||
|
||||
// Send webhook if configured
|
||||
if (template?.webhook_url) {
|
||||
try {
|
||||
await fetch(template.webhook_url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
event: "consulting_reminder",
|
||||
slot_id: slot.id,
|
||||
...payload,
|
||||
}),
|
||||
});
|
||||
console.log("Webhook sent for slot:", slot.id);
|
||||
} catch (webhookError) {
|
||||
console.error("Webhook error:", webhookError);
|
||||
}
|
||||
}
|
||||
|
||||
// Send email if template is active and Mailketing is configured
|
||||
if (template?.is_active && smtpSettings?.api_token && profile?.email) {
|
||||
try {
|
||||
// Replace shortcodes in email body using master template system
|
||||
let emailBody = template.email_body_html || "";
|
||||
let emailSubject = template.email_subject || "Reminder Konsultasi";
|
||||
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
const regex = new RegExp(`\\{${key}\\}`, "g");
|
||||
emailBody = emailBody.replace(regex, String(value));
|
||||
emailSubject = emailSubject.replace(regex, String(value));
|
||||
});
|
||||
|
||||
// Send via send-email-v2 (Mailketing API)
|
||||
const { error: emailError } = await supabase.functions.invoke("send-email-v2", {
|
||||
body: {
|
||||
recipient: profile.email,
|
||||
api_token: smtpSettings.api_token,
|
||||
from_name: smtpSettings.from_name || platformSettings?.brand_name || "Access Hub",
|
||||
from_email: smtpSettings.from_email || "noreply@with.dwindi.com",
|
||||
subject: emailSubject,
|
||||
content: emailBody,
|
||||
},
|
||||
});
|
||||
|
||||
if (emailError) {
|
||||
console.error("Failed to send reminder email:", emailError);
|
||||
} else {
|
||||
console.log("Reminder email sent to:", profile.email);
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error("Error sending reminder email:", emailError);
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
slot_id: slot.id,
|
||||
client: profile?.full_name,
|
||||
date: slot.date,
|
||||
time: slot.start_time,
|
||||
reminded: true,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: `Processed ${results.length} consultation reminders`,
|
||||
results
|
||||
}),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error sending reminders:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
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": "*",
|
||||
@@ -7,22 +8,24 @@ const corsHeaders = {
|
||||
|
||||
interface EmailRequest {
|
||||
recipient: string;
|
||||
api_token: string;
|
||||
from_name: string;
|
||||
from_email: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// Send via Mailketing API
|
||||
async function sendViaMailketing(request: EmailRequest): Promise<{ success: boolean; message: string }> {
|
||||
const { recipient, api_token, from_name, from_email, subject, content } = request;
|
||||
async function sendViaMailketing(
|
||||
request: EmailRequest,
|
||||
apiToken: string,
|
||||
fromName: string,
|
||||
fromEmail: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const { recipient, subject, content } = request;
|
||||
|
||||
// Build form-encoded body (http_build_query format)
|
||||
const params = new URLSearchParams();
|
||||
params.append('api_token', api_token);
|
||||
params.append('from_name', from_name);
|
||||
params.append('from_email', from_email);
|
||||
params.append('api_token', apiToken);
|
||||
params.append('from_name', fromName);
|
||||
params.append('from_email', fromEmail);
|
||||
params.append('recipient', recipient);
|
||||
params.append('subject', subject);
|
||||
params.append('content', content);
|
||||
@@ -58,19 +61,46 @@ serve(async (req: Request): Promise<Response> => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize Supabase client
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Fetch email settings from platform_settings
|
||||
const { data: settings, error: settingsError } = await supabase
|
||||
.from('platform_settings')
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (settingsError || !settings) {
|
||||
console.error('Error fetching platform settings:', settingsError);
|
||||
throw new Error('Failed to fetch email configuration from platform_settings');
|
||||
}
|
||||
|
||||
const apiToken = settings.integration_email_api_token;
|
||||
const fromName = settings.integration_email_from_name || settings.brand_name;
|
||||
const fromEmail = settings.integration_email_from_email;
|
||||
|
||||
if (!apiToken || !fromEmail) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Email not configured. Please set API token and from email in platform settings." }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const body: EmailRequest = await req.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!body.recipient || !body.api_token || !body.from_name || !body.from_email || !body.subject || !body.content) {
|
||||
if (!body.recipient || !body.subject || !body.content) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Missing required fields: recipient, api_token, from_name, from_email, subject, content" }),
|
||||
JSON.stringify({ success: false, message: "Missing required fields: recipient, subject, content" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(body.recipient) || !emailRegex.test(body.from_email)) {
|
||||
if (!emailRegex.test(body.recipient) || !emailRegex.test(fromEmail)) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Invalid email format" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
@@ -78,10 +108,10 @@ serve(async (req: Request): Promise<Response> => {
|
||||
}
|
||||
|
||||
console.log(`Attempting to send email to: ${body.recipient}`);
|
||||
console.log(`From: ${body.from_name} <${body.from_email}>`);
|
||||
console.log(`From: ${fromName} <${fromEmail}>`);
|
||||
console.log(`Subject: ${body.subject}`);
|
||||
|
||||
const result = await sendViaMailketing(body);
|
||||
const result = await sendViaMailketing(body, apiToken, fromName, fromEmail);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(result),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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": "*",
|
||||
@@ -259,44 +258,29 @@ serve(async (req: Request): Promise<Response> => {
|
||||
);
|
||||
}
|
||||
|
||||
// Get platform settings
|
||||
const { data: settings } = await supabase
|
||||
// Get platform settings (includes email configuration)
|
||||
const { data: platformSettings, error: platformError } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (!settings) {
|
||||
if (platformError || !platformSettings) {
|
||||
console.error('Error fetching platform settings:', platformError);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Platform settings not configured" }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const brandName = platformSettings.brand_name || "ACCESS HUB";
|
||||
|
||||
// Build email payload
|
||||
const allVariables = {
|
||||
recipient_name: recipient_name || "Pelanggan",
|
||||
platform_name: settings.brand_name || "Platform",
|
||||
platform_name: brandName,
|
||||
...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);
|
||||
|
||||
@@ -304,67 +288,30 @@ serve(async (req: Request): Promise<Response> => {
|
||||
const htmlBody = EmailTemplateRenderer.render({
|
||||
subject: subject,
|
||||
content: htmlContent,
|
||||
brandName: settings.brand_name || "ACCESS HUB",
|
||||
brandName: brandName,
|
||||
});
|
||||
|
||||
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",
|
||||
from_name: platformSettings.integration_email_from_name || brandName || "Notifikasi",
|
||||
from_email: platformSettings.integration_email_from_email || "noreply@example.com",
|
||||
};
|
||||
|
||||
// Determine provider and send
|
||||
const provider = settings.integration_email_provider || "mailketing";
|
||||
const provider = platformSettings.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;
|
||||
const mailketingToken = platformSettings.integration_email_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}`);
|
||||
throw new Error(`Unknown email provider: ${provider}. Only 'mailketing' is supported.`);
|
||||
}
|
||||
|
||||
// Log notification
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
interface TestEmailRequest {
|
||||
to: string;
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
smtp_username: string;
|
||||
smtp_password: string;
|
||||
smtp_from_name: string;
|
||||
smtp_from_email: string;
|
||||
smtp_use_tls: boolean;
|
||||
}
|
||||
|
||||
async function sendEmail(config: TestEmailRequest): Promise<{ success: boolean; message: string }> {
|
||||
const { to, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from_name, smtp_from_email, smtp_use_tls } = config;
|
||||
|
||||
// Build email content
|
||||
const boundary = "----=_Part_" + Math.random().toString(36).substr(2, 9);
|
||||
const emailContent = [
|
||||
`From: "${smtp_from_name}" <${smtp_from_email}>`,
|
||||
`To: ${to}`,
|
||||
`Subject: =?UTF-8?B?${btoa("Email Uji Coba - Konfigurasi SMTP Berhasil")}?=`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
||||
``,
|
||||
`--${boundary}`,
|
||||
`Content-Type: text/plain; charset=UTF-8`,
|
||||
``,
|
||||
`Ini adalah email uji coba dari sistem notifikasi Anda.`,
|
||||
`Jika Anda menerima email ini, konfigurasi SMTP Anda sudah benar.`,
|
||||
``,
|
||||
`--${boundary}`,
|
||||
`Content-Type: text/html; charset=UTF-8`,
|
||||
``,
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #0066cc;">Email Uji Coba Berhasil! ✓</h2>
|
||||
<p>Ini adalah email uji coba dari sistem notifikasi Anda.</p>
|
||||
<p>Jika Anda menerima email ini, konfigurasi SMTP Anda sudah benar.</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="font-size: 12px; color: #666;">
|
||||
Dikirim dari: ${smtp_from_email}<br>
|
||||
Server: ${smtp_host}:${smtp_port}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
`--${boundary}--`,
|
||||
].join("\r\n");
|
||||
|
||||
// Connect to SMTP server
|
||||
const conn = smtp_use_tls
|
||||
? await Deno.connectTls({ hostname: smtp_host, port: smtp_port })
|
||||
: await Deno.connect({ hostname: smtp_host, port: smtp_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 {
|
||||
// Read greeting
|
||||
await readResponse();
|
||||
|
||||
// EHLO
|
||||
let response = await sendCommand(`EHLO localhost`);
|
||||
console.log("EHLO response:", response);
|
||||
|
||||
// For non-TLS connection on port 587, we may need STARTTLS
|
||||
if (!smtp_use_tls && response.includes("STARTTLS")) {
|
||||
await sendCommand("STARTTLS");
|
||||
// Upgrade to TLS - not supported in basic Deno.connect
|
||||
// For now, recommend using TLS directly
|
||||
}
|
||||
|
||||
// AUTH LOGIN
|
||||
response = await sendCommand("AUTH LOGIN");
|
||||
console.log("AUTH response:", response);
|
||||
|
||||
// Username (base64)
|
||||
response = await sendCommand(btoa(smtp_username));
|
||||
console.log("Username response:", response);
|
||||
|
||||
// Password (base64)
|
||||
response = await sendCommand(btoa(smtp_password));
|
||||
console.log("Password response:", response);
|
||||
|
||||
if (!response.includes("235") && !response.includes("Authentication successful")) {
|
||||
throw new Error("Authentication failed: " + response);
|
||||
}
|
||||
|
||||
// MAIL FROM
|
||||
response = await sendCommand(`MAIL FROM:<${smtp_from_email}>`);
|
||||
if (!response.includes("250")) {
|
||||
throw new Error("MAIL FROM failed: " + response);
|
||||
}
|
||||
|
||||
// RCPT TO
|
||||
response = await sendCommand(`RCPT TO:<${to}>`);
|
||||
if (!response.includes("250")) {
|
||||
throw new Error("RCPT TO failed: " + response);
|
||||
}
|
||||
|
||||
// DATA
|
||||
response = await sendCommand("DATA");
|
||||
if (!response.includes("354")) {
|
||||
throw new Error("DATA failed: " + response);
|
||||
}
|
||||
|
||||
// Send email content
|
||||
await conn.write(encoder.encode(emailContent + "\r\n.\r\n"));
|
||||
response = await readResponse();
|
||||
if (!response.includes("250")) {
|
||||
throw new Error("Email send failed: " + response);
|
||||
}
|
||||
|
||||
// QUIT
|
||||
await sendCommand("QUIT");
|
||||
conn.close();
|
||||
|
||||
return { success: true, message: "Email uji coba berhasil dikirim ke " + to };
|
||||
} catch (error) {
|
||||
conn.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const body: TestEmailRequest = await req.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!body.to || !body.smtp_host || !body.smtp_username || !body.smtp_password) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: "Missing required fields" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Attempting to send test email to:", body.to);
|
||||
console.log("SMTP config:", { host: body.smtp_host, port: body.smtp_port, user: body.smtp_username });
|
||||
|
||||
const result = await sendEmail(body);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(result),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error sending test email:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message || "Failed to send email" }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
-- ============================================================================
|
||||
-- Update Auth OTP Email Template - Better Copywriting with Dedicated Page Link
|
||||
-- ============================================================================
|
||||
|
||||
-- Update auth_email_verification template with improved copywriting
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
email_subject = 'Konfirmasi Email Anda - {platform_name}',
|
||||
email_body_html = '---
|
||||
<h1>🔐 Konfirmasi Alamat Email</h1>
|
||||
|
||||
<p>Selamat datang di <strong>{platform_name}</strong>!</p>
|
||||
|
||||
<p>Terima kasih telah mendaftar. Untuk mengaktifkan akun Anda, masukkan kode verifikasi 6 digit berikut:</p>
|
||||
|
||||
<div class="otp-box">{otp_code}</div>
|
||||
|
||||
<p><strong>⏰ Berlaku selama {expiry_minutes} menit</strong></p>
|
||||
|
||||
<h2>🎯 Cara Verifikasi:</h2>
|
||||
<ol>
|
||||
<li><strong>Kembali ke halaman pendaftaran</strong> - Form OTP sudah otomatis muncul</li>
|
||||
<li><strong>Masukkan kode 6 digit</strong> di atas pada kolom verifikasi</li>
|
||||
<li><strong>Klik "Verifikasi Email"</strong> dan akun Anda siap digunakan!</li>
|
||||
</ol>
|
||||
|
||||
<h2>🔄 Halaman Khusus Verifikasi</h2>
|
||||
<p>Jika Anda kehilangan halaman pendaftaran atau tertutup tidak sengaja, jangan khawatir! Anda tetap bisa memverifikasi akun melalui:</p>
|
||||
|
||||
<p class="text-center" style="margin: 20px 0;">
|
||||
<a href="{platform_url}/confirm-otp?user_id={user_id}&email={email}" class="button" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
📧 Buka Halaman Verifikasi Khusus
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
<em>Link ini akan membawa Anda ke halaman khusus untuk memasukkan kode verifikasi.</em>
|
||||
</p>
|
||||
|
||||
<div class="alert-warning" style="margin: 20px 0; padding: 15px; background-color: #fff3cd; border-left: 4px solid #ffc107;">
|
||||
<p style="margin: 0;"><strong>💡 Tips:</strong> Cek folder <em>Spam</em> atau <em>Promotions</em> jika email tidak muncul di inbox dalam 1-2 menit.</p>
|
||||
</div>
|
||||
|
||||
<blockquote class="alert-info">
|
||||
<strong>ℹ️ Info:</strong> Jika Anda tidak merasa mendaftar di {platform_name}, abaikan email ini dengan aman.
|
||||
</blockquote>
|
||||
---'
|
||||
WHERE key = 'auth_email_verification';
|
||||
|
||||
-- Return success message
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Auth email template updated with improved copywriting and dedicated page link';
|
||||
END $$;
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Update order_created email template to include QR code
|
||||
-- This migration adds the QR code section to the order confirmation email
|
||||
-- Update order_created email template to remove QR code
|
||||
-- QR code is now displayed on the order detail page instead
|
||||
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
@@ -12,28 +12,6 @@ SET
|
||||
|
||||
<p>Terima kasih telah melakukan pemesanan! Berikut adalah detail pesanan Anda:</p>
|
||||
|
||||
<!-- QR Code Section -->
|
||||
<div style="text-align: center; margin: 30px 0; padding: 20px; background-color: #f5f5f5; border-radius: 8px;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 18px; color: #333;">Scan QR untuk Pembayaran</h3>
|
||||
|
||||
<!-- QR Code Image -->
|
||||
<img src="{qr_code_image}" alt="QRIS Payment QR Code" style="width: 200px; height: 200px; border: 2px solid #000; padding: 10px; background-color: #fff; display: inline-block;" />
|
||||
|
||||
<p style="margin: 15px 0 5px 0; font-size: 14px; color: #666;">
|
||||
Scan dengan aplikasi e-wallet atau mobile banking Anda
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0 0 0; font-size: 12px; color: #999;">
|
||||
Berlaku hingga: {qr_expiry_time}
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<a href="{payment_link}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Bayar Sekarang
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Summary Section -->
|
||||
<div style="margin: 30px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 16px; color: #333;">Detail Pesanan</h3>
|
||||
@@ -59,6 +37,16 @@ SET
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; background-color: #f5f5f5; border-radius: 8px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #666;">
|
||||
Silakan selesaikan pembayaran Anda dengan mengklik tombol di bawah:
|
||||
</p>
|
||||
|
||||
<a href="{payment_link}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Bayar Sekarang
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Silakan selesaikan pembayaran Anda sebelum waktu habis. Setelah pembayaran berhasil, akses ke produk akan segera aktif.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ============================================================================
|
||||
-- Add platform_url column to platform_settings
|
||||
-- ============================================================================
|
||||
|
||||
-- Add platform_url column if it doesn't exist
|
||||
ALTER TABLE platform_settings
|
||||
ADD COLUMN IF NOT EXISTS platform_url TEXT;
|
||||
|
||||
-- Set default value if null
|
||||
UPDATE platform_settings
|
||||
SET platform_url = 'https://access-hub.com'
|
||||
WHERE platform_url IS NULL;
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON COLUMN platform_settings.platform_url IS 'Base URL of the platform (used for email links)';
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Add test_email template for "Uji Coba Email" button in Integrasi tab
|
||||
INSERT INTO notification_templates (key, name, email_subject, email_body_html, is_active, created_at, updated_at)
|
||||
VALUES (
|
||||
'test_email',
|
||||
'Test Email',
|
||||
'Email Test - {platform_name}',
|
||||
'
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #333;">Email Test - {platform_name}</h2>
|
||||
|
||||
<p>Halo,</p>
|
||||
|
||||
<p>Ini adalah email tes dari sistem <strong>{platform_name}</strong>.</p>
|
||||
|
||||
<div style="margin: 20px 0; padding: 15px; background-color: #f0f0f0; border-left: 4px solid #000;">
|
||||
<p style="margin: 0; font-size: 14px;">
|
||||
<strong>✓ Konfigurasi email berhasil!</strong><br>
|
||||
Email Anda telah terkirim dengan benar menggunakan provider: <strong>Mailketing</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Jika Anda menerima email ini, berarti konfigurasi email sudah benar.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px;">
|
||||
Terima kasih,<br>
|
||||
Tim {platform_name}
|
||||
</p>
|
||||
</div>
|
||||
',
|
||||
true,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
email_subject = EXCLUDED.email_subject,
|
||||
email_body_html = EXCLUDED.email_body_html,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Verify the template
|
||||
SELECT
|
||||
key,
|
||||
name,
|
||||
email_subject,
|
||||
is_active
|
||||
FROM notification_templates
|
||||
WHERE key = 'test_email';
|
||||
@@ -0,0 +1,197 @@
|
||||
-- ============================================================================
|
||||
-- Fix Email Templates: Use Short Order ID and Add Missing Links
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Fix order_created template - use short order_id and fix subject
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
email_subject = 'Konfirmasi Pesanan - #{order_id_short}',
|
||||
email_body_html = '---
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #333;">Konfirmasi Pesanan</h2>
|
||||
|
||||
<p>Halo {nama},</p>
|
||||
|
||||
<p>Terima kasih telah melakukan pemesanan! Berikut adalah detail pesanan Anda:</p>
|
||||
|
||||
<!-- Order Summary Section -->
|
||||
<div style="margin: 30px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 16px; color: #333;">Detail Pesanan</h3>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Order ID:</strong> #{order_id_short}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Tanggal:</strong> {tanggal_pesanan}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Produk:</strong> {produk}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Metode Pembayaran:</strong> {metode_pembayaran}
|
||||
</p>
|
||||
|
||||
<p style="margin: 15px 0 5px 0; font-size: 16px; font-weight: bold; color: #000;">
|
||||
Total: {total}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; background-color: #f5f5f5; border-radius: 8px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #666;">
|
||||
Silakan selesaikan pembayaran Anda dengan mengklik tombol di bawah:
|
||||
</p>
|
||||
|
||||
<a href="{payment_link}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Bayar Sekarang
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Silakan selesaikan pembayaran Anda sebelum waktu habis. Setelah pembayaran berhasil, akses ke produk akan segera aktif.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px;">
|
||||
Jika Anda memiliki pertanyaan, jangan ragu untuk menghubungi kami.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px;">
|
||||
Terima kasih,<br>
|
||||
Tim {platform_name}
|
||||
</p>
|
||||
</div>
|
||||
---',
|
||||
updated_at = NOW()
|
||||
WHERE key = 'order_created';
|
||||
|
||||
-- 2. Create or update payment_success template
|
||||
INSERT INTO notification_templates (key, name, email_subject, email_body_html, is_active, created_at, updated_at)
|
||||
VALUES (
|
||||
'payment_success',
|
||||
'Payment Success Email',
|
||||
'Pembayaran Berhasil - Order #{order_id_short}',
|
||||
'---
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #28a745;">Pembayaran Berhasil! ✓</h2>
|
||||
|
||||
<p>Halo {nama},</p>
|
||||
|
||||
<p>Terima kasih! Pembayaran Anda telah berhasil dikonfirmasi.</p>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 16px; color: #333;">Detail Pesanan</h3>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Order ID:</strong> #{order_id_short}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Tanggal:</strong> {tanggal_pesanan}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Produk:</strong> {produk}
|
||||
</p>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>Metode Pembayaran:</strong> {metode_pembayaran}
|
||||
</p>
|
||||
|
||||
<p style="margin: 15px 0 5px 0; font-size: 16px; font-weight: bold; color: #28a745;">
|
||||
Total: {total}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; background-color: #f0f8ff; border-radius: 8px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #666;">
|
||||
Akses ke produk Anda sudah aktif! Klik tombol di bawah untuk mulai belajar:
|
||||
</p>
|
||||
|
||||
<a href="{link_akses}" style="display: inline-block; padding: 12px 24px; background-color: #28a745; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Akses Sekarang
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Jika Anda mengalami masalah saat mengakses produk, jangan ragu untuk menghubungi kami.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px;">
|
||||
Selamat belajar!<br>
|
||||
Tim {platform_name}
|
||||
</p>
|
||||
</div>
|
||||
---',
|
||||
true,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
email_subject = EXCLUDED.email_subject,
|
||||
email_body_html = EXCLUDED.email_body_html,
|
||||
is_active = EXCLUDED.is_active,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 3. Create or update access_granted template
|
||||
INSERT INTO notification_templates (key, name, email_subject, email_body_html, is_active, created_at, updated_at)
|
||||
VALUES (
|
||||
'access_granted',
|
||||
'Access Granted Email',
|
||||
'Akses Produk Diberikan - {produk}',
|
||||
'---
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #28a745;">Akses Produk Aktif! 🎉</h2>
|
||||
|
||||
<p>Halo {nama},</p>
|
||||
|
||||
<p>Selamat! Akses ke produk Anda telah diaktifkan.</p>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; background-color: #f0f8ff; border: 1px solid #b3d9ff; border-radius: 8px;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 16px; color: #333;">Produk Anda:</h3>
|
||||
|
||||
<p style="margin: 5px 0; font-size: 14px;">
|
||||
<strong>{produk}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 30px 0; padding: 20px; background-color: #f0f8ff; border-radius: 8px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #666;">
|
||||
Mulai belajar sekarang dengan mengklik tombol di bawah:
|
||||
</p>
|
||||
|
||||
<a href="{link_akses}" style="display: inline-block; padding: 12px 24px; background-color: #28a745; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Akses Sekarang
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Nikmati pembelajaran Anda! Jika ada pertanyaan, jangan ragu untuk menghubungi kami.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 14px;">
|
||||
Happy learning!<br>
|
||||
Tim {platform_name}
|
||||
</p>
|
||||
</div>
|
||||
---',
|
||||
true,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
email_subject = EXCLUDED.email_subject,
|
||||
email_body_html = EXCLUDED.email_body_html,
|
||||
is_active = EXCLUDED.is_active,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Verify updates
|
||||
SELECT
|
||||
key,
|
||||
email_subject,
|
||||
is_active,
|
||||
updated_at
|
||||
FROM notification_templates
|
||||
WHERE key IN ('order_created', 'payment_success', 'access_granted')
|
||||
ORDER BY key;
|
||||
@@ -218,11 +218,7 @@ export class EmailTemplateRenderer {
|
||||
<tr>
|
||||
<td align="left" style="font-size: 12px; line-height: 18px; font-family: monospace; color: #555;">
|
||||
<p style="margin: 0 0 10px 0; font-weight: bold;">{{brandName}}</p>
|
||||
<p style="margin: 0 0 15px 0;">Email ini dikirim otomatis. Jangan membalas email ini.</p>
|
||||
<p style="margin: 0;">
|
||||
<a href="#" style="color: #000; text-decoration: underline;">Ubah Preferensi</a> |
|
||||
<a href="#" style="color: #000; text-decoration: underline;">Unsubscribe</a>
|
||||
</p>
|
||||
<p style="margin: 0;">Email ini dikirim otomatis. Jangan membalas email ini.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user