(emptySettings);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ fetchSettings();
+ }, []);
+
+ const fetchSettings = async () => {
+ const { data, error } = await supabase
+ .from('platform_settings')
+ .select('*')
+ .single();
+
+ if (data) {
+ setSettings({
+ id: data.id,
+ integration_n8n_base_url: data.integration_n8n_base_url || '',
+ integration_whatsapp_number: data.integration_whatsapp_number || '',
+ integration_whatsapp_url: data.integration_whatsapp_url || '',
+ integration_google_calendar_id: data.integration_google_calendar_id || '',
+ integration_email_provider: data.integration_email_provider || 'smtp',
+ integration_email_api_base_url: data.integration_email_api_base_url || '',
+ integration_privacy_url: data.integration_privacy_url || '/privacy',
+ integration_terms_url: data.integration_terms_url || '/terms',
+ });
+ }
+ setLoading(false);
+ };
+
+ const saveSettings = async () => {
+ setSaving(true);
+ const payload = { ...settings };
+ delete payload.id;
+
+ if (settings.id) {
+ const { error } = await supabase
+ .from('platform_settings')
+ .update(payload)
+ .eq('id', settings.id);
+
+ if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
+ else toast({ title: 'Berhasil', description: 'Pengaturan integrasi disimpan' });
+ } else {
+ const { data, error } = await supabase
+ .from('platform_settings')
+ .insert(payload)
+ .select()
+ .single();
+
+ if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
+ else {
+ setSettings({ ...settings, id: data.id });
+ toast({ title: 'Berhasil', description: 'Pengaturan integrasi disimpan' });
+ }
+ }
+ setSaving(false);
+ };
+
+ if (loading) return
;
+
+ return (
+
+ {/* n8n / Webhook */}
+
+
+
+
+ n8n / Webhook
+
+
+ Konfigurasi URL untuk integrasi otomatisasi
+
+
+
+
+
Base URL n8n / Integrasi
+
setSettings({ ...settings, integration_n8n_base_url: e.target.value })}
+ placeholder="https://automation.domain.com"
+ className="border-2"
+ />
+
+ Digunakan sebagai target default untuk webhook lanjutan. webhook_url per template tetap harus URL lengkap.
+
+
+
+
+
+ {/* WhatsApp */}
+
+
+
+
+ WhatsApp
+
+
+ Nomor kontak untuk dukungan dan notifikasi
+
+
+
+
+
+
Nomor WhatsApp Dukungan
+
setSettings({ ...settings, integration_whatsapp_number: e.target.value })}
+ placeholder="+62812xxxx"
+ className="border-2"
+ />
+
+ Ditampilkan di konfirmasi konsultasi
+
+
+
+
+ URL WhatsApp Click-to-Chat (Opsional)
+ setSettings({ ...settings, integration_whatsapp_url: e.target.value })}
+ placeholder="https://wa.me/62812..."
+ className="border-2"
+ />
+
+
+
+
+
+ {/* Google Calendar */}
+
+
+
+
+ Google Calendar
+
+
+ Untuk pembuatan event konsultasi otomatis
+
+
+
+
+
ID Kalender Google untuk Konsultasi
+
setSettings({ ...settings, integration_google_calendar_id: e.target.value })}
+ placeholder="your-calendar@gmail.com"
+ className="border-2"
+ />
+
+ Backend/n8n akan menggunakan ID ini untuk membuat event
+
+
+
+
+
+ {/* Email Provider */}
+
+
+
+
+ Provider Email (Opsional)
+
+
+ Konfigurasi alternatif selain SMTP
+
+
+
+
+
+ Provider Email Eksternal
+ setSettings({ ...settings, integration_email_provider: value })}
+ >
+
+
+
+
+ SMTP (Default)
+ Resend
+ Mailgun
+ SendGrid
+
+
+
+
+
+ API Base URL Provider Email
+ setSettings({ ...settings, integration_email_api_base_url: e.target.value })}
+ placeholder="https://api.resend.com"
+ className="border-2"
+ disabled={settings.integration_email_provider === 'smtp'}
+ />
+
+
+
+
+
+ {/* Public Links */}
+
+
+
+
+ Link Publik
+
+
+ URL untuk halaman legal
+
+
+
+
+
+ Default: halaman internal. Bisa diganti dengan URL eksternal.
+
+
+
+
+
+ {saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
+
+
+ );
+}
diff --git a/src/components/admin/settings/NotifikasiTab.tsx b/src/components/admin/settings/NotifikasiTab.tsx
index 07a3769..75f7dce 100644
--- a/src/components/admin/settings/NotifikasiTab.tsx
+++ b/src/components/admin/settings/NotifikasiTab.tsx
@@ -41,14 +41,49 @@ const SHORTCODES_HELP = {
event: ['{judul_event}', '{tanggal_event}', '{jam_event}', '{link_event}'],
};
-const DEFAULT_TEMPLATES = [
- { key: 'payment_success', name: 'Pembayaran Berhasil' },
- { key: 'access_granted', name: 'Akses Produk Diberikan' },
- { key: 'order_created', name: 'Pesanan Dibuat' },
- { key: 'payment_reminder', name: 'Pengingat Pembayaran' },
- { key: 'consulting_scheduled', name: 'Konsultasi Terjadwal' },
- { key: 'event_reminder', name: 'Reminder Webinar/Bootcamp' },
- { key: 'bootcamp_progress', name: 'Progress Bootcamp' },
+const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; defaultBody: string }[] = [
+ {
+ key: 'payment_success',
+ name: 'Pembayaran Berhasil',
+ defaultSubject: 'Pembayaran Berhasil - Order #{order_id}',
+ defaultBody: 'Halo {nama}! Terima kasih, pembayaran Anda sebesar {total} telah berhasil dikonfirmasi.
Detail Pesanan:
Order ID: {order_id} Tanggal: {tanggal_pesanan} Metode: {metode_pembayaran} Produk: {produk}
'
+ },
+ {
+ key: 'access_granted',
+ name: 'Akses Produk Diberikan',
+ defaultSubject: 'Akses Anda Sudah Aktif - {produk}',
+ defaultBody: 'Halo {nama}! Selamat! Akses Anda ke {produk} sudah aktif.
Akses Sekarang
'
+ },
+ {
+ key: 'order_created',
+ name: 'Pesanan Dibuat',
+ defaultSubject: 'Pesanan Anda #{order_id} Sedang Diproses',
+ defaultBody: 'Halo {nama}! Pesanan Anda dengan nomor {order_id} telah kami terima.
Total: {total}
Silakan selesaikan pembayaran sebelum batas waktu.
'
+ },
+ {
+ key: 'payment_reminder',
+ name: 'Pengingat Pembayaran',
+ defaultSubject: 'Jangan Lupa Bayar - Order #{order_id}',
+ defaultBody: 'Halo {nama}! Pesanan Anda dengan nomor {order_id} menunggu pembayaran.
Total: {total}
Segera selesaikan pembayaran agar tidak kedaluwarsa.
'
+ },
+ {
+ key: 'consulting_scheduled',
+ name: 'Konsultasi Terjadwal',
+ defaultSubject: 'Konsultasi Anda Sudah Terjadwal - {tanggal_konsultasi}',
+ defaultBody: 'Halo {nama}! Sesi konsultasi Anda telah dikonfirmasi:
Tanggal: {tanggal_konsultasi} Jam: {jam_konsultasi} Link meeting: {link_meet}
Jika ada pertanyaan, hubungi kami.
'
+ },
+ {
+ key: 'event_reminder',
+ name: 'Reminder Webinar/Bootcamp',
+ defaultSubject: 'Reminder: {judul_event} Dimulai {tanggal_event}',
+ defaultBody: 'Halo {nama}! Jangan lupa, {judul_event} akan dimulai:
Tanggal: {tanggal_event} Jam: {jam_event} Bergabung
'
+ },
+ {
+ key: 'bootcamp_progress',
+ name: 'Progress Bootcamp',
+ defaultSubject: 'Update Progress Bootcamp Anda',
+ defaultBody: 'Halo {nama}! Ini adalah update progress bootcamp Anda.
Terus semangat belajar!
'
+ },
];
const emptySmtp: SmtpSettings = {
@@ -94,8 +129,8 @@ export function NotifikasiTab() {
key: t.key,
name: t.name,
is_active: false,
- email_subject: '',
- email_body_html: '',
+ email_subject: t.defaultSubject,
+ email_body_html: t.defaultBody,
webhook_url: '',
}));
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
@@ -261,8 +296,8 @@ export function NotifikasiTab() {
-
-
Shortcode yang tersedia:
+
+
Shortcode yang tersedia:
Umum: {SHORTCODES_HELP.common.join(', ')}
@@ -277,6 +312,11 @@ export function NotifikasiTab() {
Event: {SHORTCODES_HELP.event.join(', ')}
+
+ Penting: Toggle "Aktifkan" hanya mengontrol pengiriman email.
+ Jika webhook_url diisi, sistem tetap akan mengirim payload ke URL tersebut
+ meskipun email dinonaktifkan.
+
{templates.map((template) => (
diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx
index 49a2f22..beddaae 100644
--- a/src/pages/Products.tsx
+++ b/src/pages/Products.tsx
@@ -9,6 +9,7 @@ import { useCart } from '@/contexts/CartContext';
import { toast } from '@/hooks/use-toast';
import { Skeleton } from '@/components/ui/skeleton';
import { formatIDR } from '@/lib/format';
+import { Video } from 'lucide-react';
interface Product {
id: string;
@@ -21,27 +22,45 @@ interface Product {
is_active: boolean;
}
+interface ConsultingSettings {
+ is_consulting_enabled: boolean;
+ consulting_block_price: number;
+ consulting_block_duration_minutes: number;
+}
+
export default function Products() {
const [products, setProducts] = useState
([]);
+ const [consultingSettings, setConsultingSettings] = useState(null);
const [loading, setLoading] = useState(true);
const { addItem, items } = useCart();
useEffect(() => {
- fetchProducts();
+ fetchData();
}, []);
- const fetchProducts = async () => {
- const { data, error } = await supabase
- .from('products')
- .select('*')
- .eq('is_active', true)
- .order('created_at', { ascending: false });
+ const fetchData = async () => {
+ const [productsRes, consultingRes] = await Promise.all([
+ supabase
+ .from('products')
+ .select('*')
+ .eq('is_active', true)
+ .order('created_at', { ascending: false }),
+ supabase
+ .from('consulting_settings')
+ .select('is_consulting_enabled, consulting_block_price, consulting_block_duration_minutes')
+ .single(),
+ ]);
- if (error) {
+ if (productsRes.error) {
toast({ title: 'Error', description: 'Gagal memuat produk', variant: 'destructive' });
} else {
- setProducts(data || []);
+ setProducts(productsRes.data || []);
}
+
+ if (consultingRes.data) {
+ setConsultingSettings(consultingRes.data);
+ }
+
setLoading(false);
};
@@ -87,12 +106,42 @@ export default function Products() {
))}
- ) : products.length === 0 ? (
-
-
Belum ada produk tersedia.
-
) : (
+ {/* Consulting Card - Only show when enabled */}
+ {consultingSettings?.is_consulting_enabled && (
+
+
+
+
+
+ Konsultasi 1-on-1
+
+ Konsultasi
+
+
+ Sesi konsultasi pribadi dengan mentor. Pilih waktu dan durasi sesuai kebutuhan Anda.
+
+
+
+
+
+ {formatIDR(consultingSettings.consulting_block_price)}
+
+
+ / {consultingSettings.consulting_block_duration_minutes} menit
+
+
+
+
+ Booking Sekarang
+
+
+
+
+ )}
+
+ {/* Regular Products */}
{products.map((product) => (
@@ -128,6 +177,12 @@ export default function Products() {
))}
+
+ {products.length === 0 && !consultingSettings?.is_consulting_enabled && (
+
+
Belum ada produk tersedia.
+
+ )}
)}
diff --git a/src/pages/admin/AdminSettings.tsx b/src/pages/admin/AdminSettings.tsx
index 24517cf..521d8e1 100644
--- a/src/pages/admin/AdminSettings.tsx
+++ b/src/pages/admin/AdminSettings.tsx
@@ -7,6 +7,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { WorkhoursTab } from '@/components/admin/settings/WorkhoursTab';
import { NotifikasiTab } from '@/components/admin/settings/NotifikasiTab';
import { KonsultasiTab } from '@/components/admin/settings/KonsultasiTab';
+import { BrandingTab } from '@/components/admin/settings/BrandingTab';
+import { IntegrasiTab } from '@/components/admin/settings/IntegrasiTab';
import { Clock, Bell, Video, Palette, Puzzle } from 'lucide-react';
export default function AdminSettings() {
@@ -51,11 +53,11 @@ export default function AdminSettings() {
Konsultasi
-
+
Branding
-
+
Integrasi
@@ -74,15 +76,11 @@ export default function AdminSettings() {
-
- Fitur Branding akan segera hadir
-
+
-
- Fitur Integrasi akan segera hadir
-
+
diff --git a/supabase/functions/pakasir-webhook/index.ts b/supabase/functions/pakasir-webhook/index.ts
index eedc554..a757f1b 100644
--- a/supabase/functions/pakasir-webhook/index.ts
+++ b/supabase/functions/pakasir-webhook/index.ts
@@ -1,22 +1,17 @@
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
-import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.1";
+import { createClient, SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2.49.1";
+import { SMTPClient } from "https://deno.land/x/denomailer@1.6.0/mod.ts";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-pakasir-signature, x-callback-token",
};
-// TODO: Set these in your Supabase Edge Function secrets
+// Environment variables
const PAKASIR_WEBHOOK_SECRET = Deno.env.get("PAKASIR_WEBHOOK_SECRET") || "";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
-// Email template placeholder - will be replaced with real provider later
-const ORDER_PAID_EMAIL_TEMPLATE = Deno.env.get("ORDER_PAID_EMAIL_TEMPLATE") || JSON.stringify({
- subject: "Pembayaran Berhasil - {{order_id}}",
- body: "Terima kasih! Pembayaran untuk pesanan {{order_id}} sebesar Rp {{amount}} telah berhasil. Anda sekarang memiliki akses ke: {{products}}."
-});
-
interface PakasirWebhookPayload {
amount: number;
order_id: string;
@@ -26,33 +21,201 @@ interface PakasirWebhookPayload {
completed_at?: string;
}
-// Placeholder email function - logs for now, will be replaced with real provider
-async function sendOrderPaidEmail(
- userEmail: string,
- order: { id: string; total_amount: number },
- products: string[]
-): Promise {
- try {
- const template = JSON.parse(ORDER_PAID_EMAIL_TEMPLATE);
-
- const subject = template.subject
- .replace("{{order_id}}", order.id.substring(0, 8))
- .replace("{{amount}}", order.total_amount.toLocaleString("id-ID"));
-
- const body = template.body
- .replace("{{order_id}}", order.id.substring(0, 8))
- .replace("{{amount}}", order.total_amount.toLocaleString("id-ID"))
- .replace("{{products}}", products.join(", "));
+interface SmtpSettings {
+ smtp_host: string;
+ smtp_port: number;
+ smtp_username: string;
+ smtp_password: string;
+ smtp_from_name: string;
+ smtp_from_email: string;
+ smtp_use_tls: boolean;
+}
- console.log("[EMAIL] Would send to:", userEmail);
- console.log("[EMAIL] Subject:", subject);
- console.log("[EMAIL] Body:", body);
-
- // TODO: Replace with actual email provider call (e.g., Resend, SendGrid)
- // await emailProvider.send({ to: userEmail, subject, body });
- } catch (error) {
- console.error("[EMAIL] Error preparing email:", error);
+interface NotificationTemplate {
+ id: string;
+ key: string;
+ name: string;
+ is_active: boolean;
+ email_subject: string;
+ email_body_html: string;
+ webhook_url: string;
+}
+
+// Replace shortcodes in template
+function replaceShortcodes(template: string, data: Record): string {
+ let result = template || "";
+ for (const [key, value] of Object.entries(data)) {
+ result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value || '');
}
+ return result;
+}
+
+// Send email via SMTP
+async function sendEmail(
+ smtp: SmtpSettings,
+ to: string,
+ subject: string,
+ htmlBody: string,
+ brandFromName?: string
+): Promise {
+ if (!smtp.smtp_host || !smtp.smtp_username || !smtp.smtp_password) {
+ console.log("[EMAIL] SMTP not configured, skipping email");
+ return false;
+ }
+
+ try {
+ const client = new SMTPClient({
+ connection: {
+ hostname: smtp.smtp_host,
+ port: smtp.smtp_port,
+ tls: smtp.smtp_use_tls,
+ auth: {
+ username: smtp.smtp_username,
+ password: smtp.smtp_password,
+ },
+ },
+ });
+
+ const fromName = smtp.smtp_from_name || brandFromName || "Notification";
+ const fromEmail = smtp.smtp_from_email || smtp.smtp_username;
+
+ await client.send({
+ from: `${fromName} <${fromEmail}>`,
+ to: to,
+ subject: subject,
+ content: "auto",
+ html: htmlBody,
+ });
+
+ await client.close();
+ console.log("[EMAIL] Sent successfully to:", to);
+ return true;
+ } catch (error) {
+ console.error("[EMAIL] Failed to send:", error);
+ return false;
+ }
+}
+
+// Send webhook notification
+async function sendWebhook(url: string, payload: Record): Promise {
+ if (!url) return false;
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ console.log("[WEBHOOK] Sent to:", url, "Status:", response.status);
+ return response.ok;
+ } catch (error) {
+ console.error("[WEBHOOK] Failed to send to:", url, error);
+ return false;
+ }
+}
+
+// Process notification for a specific event
+async function processNotification(
+ supabase: SupabaseClient,
+ templateKey: string,
+ recipientEmail: string,
+ shortcodeData: Record,
+ webhookPayload: Record
+): Promise {
+ console.log(`[NOTIFICATION] Processing ${templateKey} for ${recipientEmail}`);
+
+ // Fetch template
+ const { data: templateData } = await supabase
+ .from("notification_templates")
+ .select("*")
+ .eq("key", templateKey)
+ .single();
+
+ if (!templateData) {
+ console.log(`[NOTIFICATION] Template ${templateKey} not found`);
+ return;
+ }
+
+ const template = templateData as NotificationTemplate;
+
+ // ALWAYS send webhook if URL is configured (regardless of is_active)
+ if (template.webhook_url) {
+ await sendWebhook(template.webhook_url, webhookPayload);
+
+ // Update last_payload_example
+ await supabase
+ .from("notification_templates")
+ .update({ last_payload_example: webhookPayload, updated_at: new Date().toISOString() })
+ .eq("id", template.id);
+ }
+
+ // Only send email if is_active is true
+ if (!template.is_active) {
+ console.log(`[NOTIFICATION] Template ${templateKey} is not active, skipping email`);
+ return;
+ }
+
+ // Fetch SMTP settings
+ const { data: smtpData } = await supabase
+ .from("notification_settings")
+ .select("*")
+ .single();
+
+ if (!smtpData) {
+ console.log("[NOTIFICATION] SMTP settings not configured");
+ return;
+ }
+
+ const smtpSettings = smtpData as SmtpSettings;
+
+ // Fetch brand settings for fallback from_name
+ const { data: platformData } = await supabase
+ .from("platform_settings")
+ .select("brand_email_from_name")
+ .single();
+
+ const brandFromName = (platformData as { brand_email_from_name?: string } | null)?.brand_email_from_name;
+
+ // Replace shortcodes
+ const subject = replaceShortcodes(template.email_subject, shortcodeData);
+ const body = replaceShortcodes(template.email_body_html, shortcodeData);
+
+ // Wrap body in email template
+ const fullHtml = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+ await sendEmail(
+ smtpSettings,
+ recipientEmail,
+ subject,
+ fullHtml,
+ brandFromName
+ );
}
serve(async (req) => {
@@ -143,64 +306,152 @@ serve(async (req) => {
console.log("[WEBHOOK] Order updated to paid:", order.id);
- // Get order items to grant access
- const { data: orderItems, error: itemsError } = await supabase
- .from("order_items")
- .select("product_id, product:products(title)")
- .eq("order_id", order.id);
-
- if (itemsError) {
- console.error("[WEBHOOK] Failed to fetch order items:", itemsError);
- }
-
- const productTitles: string[] = [];
-
- // Grant user_access for each product
- if (orderItems && orderItems.length > 0) {
- for (const item of orderItems) {
- const productId = item.product_id;
- // Supabase joins can return array or single object depending on relationship
- const productData = Array.isArray(item.product) ? item.product[0] : item.product;
-
- // Check if access already exists
- const { data: existingAccess } = await supabase
- .from("user_access")
- .select("id")
- .eq("user_id", order.user_id)
- .eq("product_id", productId)
- .maybeSingle();
-
- if (!existingAccess) {
- const { error: accessError } = await supabase
- .from("user_access")
- .insert({
- user_id: order.user_id,
- product_id: productId,
- });
-
- if (accessError) {
- console.error("[WEBHOOK] Failed to grant access for product:", productId, accessError);
- } else {
- console.log("[WEBHOOK] Granted access for product:", productId);
- }
- }
-
- if (productData?.title) {
- productTitles.push(productData.title);
- }
- }
- }
-
- // Get user email for notification
+ // Get user profile
const { data: profile } = await supabase
.from("profiles")
- .select("email")
+ .select("full_name, email")
.eq("id", order.user_id)
.single();
- // Send email notification (placeholder)
- if (profile?.email) {
- await sendOrderPaidEmail(profile.email, order, productTitles);
+ const userEmail = (profile as { email?: string } | null)?.email || "";
+ const userName = (profile as { full_name?: string } | null)?.full_name || "Pelanggan";
+
+ // Check for consulting slots linked to this order
+ const { data: consultingSlots } = await supabase
+ .from("consulting_slots")
+ .select("*")
+ .eq("order_id", order.id);
+
+ if (consultingSlots && consultingSlots.length > 0) {
+ // This is a consulting order - update slot statuses
+ const { error: slotUpdateError } = await supabase
+ .from("consulting_slots")
+ .update({ status: "confirmed" })
+ .eq("order_id", order.id);
+
+ if (slotUpdateError) {
+ console.error("[WEBHOOK] Failed to update consulting slots:", slotUpdateError);
+ } else {
+ console.log("[WEBHOOK] Consulting slots confirmed for order:", order.id);
+ }
+
+ // Format consulting slot details for notification
+ const slots = consultingSlots as Array<{ date: string; start_time: string; end_time: string; meet_link?: string }>;
+
+ const shortcodeData: Record = {
+ nama: userName,
+ email: userEmail,
+ order_id: order.id.substring(0, 8),
+ tanggal_pesanan: new Date().toLocaleDateString("id-ID"),
+ total: `Rp ${order.total_amount.toLocaleString("id-ID")}`,
+ metode_pembayaran: payload.payment_method || "Pakasir",
+ tanggal_konsultasi: slots[0]?.date || "",
+ jam_konsultasi: slots.map(s => s.start_time.substring(0, 5)).join(", "),
+ link_meet: slots[0]?.meet_link || "Akan dikirim terpisah",
+ };
+
+ const webhookPayload: Record = {
+ event: "consulting_scheduled",
+ order_id: order.id,
+ user_id: order.user_id,
+ user_email: userEmail,
+ user_name: userName,
+ total_amount: order.total_amount,
+ payment_method: payload.payment_method,
+ slots: consultingSlots,
+ timestamp: new Date().toISOString(),
+ };
+
+ // Send consulting_scheduled notification
+ await processNotification(supabase, "consulting_scheduled", userEmail, shortcodeData, webhookPayload);
+
+ } else {
+ // Regular product order - grant access
+ const { data: orderItems, error: itemsError } = await supabase
+ .from("order_items")
+ .select("product_id, product:products(title)")
+ .eq("order_id", order.id);
+
+ if (itemsError) {
+ console.error("[WEBHOOK] Failed to fetch order items:", itemsError);
+ }
+
+ const productTitles: string[] = [];
+
+ // Grant user_access for each product
+ if (orderItems && orderItems.length > 0) {
+ for (const item of orderItems as Array<{ product_id: string; product: { title: string } | { title: string }[] | null }>) {
+ const productId = item.product_id;
+ const productData = Array.isArray(item.product) ? item.product[0] : item.product;
+
+ // Check if access already exists
+ const { data: existingAccess } = await supabase
+ .from("user_access")
+ .select("id")
+ .eq("user_id", order.user_id)
+ .eq("product_id", productId)
+ .maybeSingle();
+
+ if (!existingAccess) {
+ const { error: accessError } = await supabase
+ .from("user_access")
+ .insert({
+ user_id: order.user_id,
+ product_id: productId,
+ });
+
+ if (accessError) {
+ console.error("[WEBHOOK] Failed to grant access for product:", productId, accessError);
+ } else {
+ console.log("[WEBHOOK] Granted access for product:", productId);
+ }
+ }
+
+ if (productData?.title) {
+ productTitles.push(productData.title);
+ }
+ }
+ }
+
+ const shortcodeData: Record = {
+ nama: userName,
+ email: userEmail,
+ order_id: order.id.substring(0, 8),
+ tanggal_pesanan: new Date().toLocaleDateString("id-ID"),
+ total: `Rp ${order.total_amount.toLocaleString("id-ID")}`,
+ metode_pembayaran: payload.payment_method || "Pakasir",
+ produk: productTitles.join(", "),
+ link_akses: `${Deno.env.get("SITE_URL") || ""}/access`,
+ };
+
+ const webhookPayload: Record = {
+ event: "payment_success",
+ order_id: order.id,
+ user_id: order.user_id,
+ user_email: userEmail,
+ user_name: userName,
+ total_amount: order.total_amount,
+ payment_method: payload.payment_method,
+ products: productTitles,
+ timestamp: new Date().toISOString(),
+ };
+
+ // Send payment_success notification
+ await processNotification(supabase, "payment_success", userEmail, shortcodeData, webhookPayload);
+
+ // Also send access_granted notification
+ if (productTitles.length > 0) {
+ const accessWebhookPayload: Record = {
+ event: "access_granted",
+ order_id: order.id,
+ user_id: order.user_id,
+ user_email: userEmail,
+ user_name: userName,
+ products: productTitles,
+ timestamp: new Date().toISOString(),
+ };
+ await processNotification(supabase, "access_granted", userEmail, shortcodeData, accessWebhookPayload);
+ }
}
return new Response(JSON.stringify({ success: true, order_id: order.id }), {