- Migrate consulting_slots to consulting_sessions structure - Add calendar_event_id to track Google Calendar events - Create delete-calendar-event edge function for auto-cleanup - Add "Tambah ke Kalender" button for members (OrderDetail, ConsultingHistory) - Update create-google-meet-event to store calendar event ID - Update handle-order-paid to use consulting_sessions table - Remove deprecated create-meet-link function - Add comprehensive documentation (CALENDAR_INTEGRATION.md, MIGRATION_GUIDE.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
327 lines
11 KiB
TypeScript
327 lines
11 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";
|
|
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
|
};
|
|
|
|
interface HandlePaidOrderRequest {
|
|
order_id: string;
|
|
user_id: string;
|
|
total_amount: number;
|
|
payment_method?: string;
|
|
payment_provider?: string;
|
|
}
|
|
|
|
serve(async (req: Request): Promise<Response> => {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const body: HandlePaidOrderRequest = await req.json();
|
|
const { order_id } = body;
|
|
|
|
console.log("[HANDLE-PAID] Processing paid order:", order_id);
|
|
console.log("[HANDLE-PAID] Request body:", JSON.stringify(body));
|
|
|
|
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
// Get full order details with items AND consulting sessions
|
|
// Use maybeSingle() in case there are no related records
|
|
const { data: order, error: orderError } = await supabase
|
|
.from("orders")
|
|
.select(`
|
|
*,
|
|
profiles(email, name),
|
|
order_items (
|
|
product_id,
|
|
product:products (title, type)
|
|
),
|
|
consulting_sessions (
|
|
id,
|
|
session_date,
|
|
start_time,
|
|
end_time,
|
|
status,
|
|
topic_category
|
|
)
|
|
`)
|
|
.eq("id", order_id)
|
|
.maybeSingle();
|
|
|
|
if (orderError) {
|
|
console.error("[HANDLE-PAID] Database error:", orderError);
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: "Database error", details: orderError.message }),
|
|
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
if (!order) {
|
|
console.error("[HANDLE-PAID] Order not found:", order_id);
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: "Order not found", order_id }),
|
|
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
console.log("[HANDLE-PAID] Order found:", JSON.stringify({
|
|
id: order.id,
|
|
payment_status: order.payment_status,
|
|
order_items_count: order.order_items?.length || 0,
|
|
consulting_sessions_count: order.consulting_sessions?.length || 0,
|
|
consulting_sessions: order.consulting_sessions
|
|
}));
|
|
|
|
const userEmail = order.profiles?.email || "";
|
|
const userName = order.profiles?.name || userEmail.split('@')[0] || "Pelanggan";
|
|
const orderItems = order.order_items as Array<{
|
|
product_id: string;
|
|
product: { title: string; type: string };
|
|
}>;
|
|
|
|
// Check if this is a consulting order by checking consulting_sessions
|
|
const consultingSessions = order.consulting_sessions as Array<{
|
|
id: string;
|
|
session_date: string;
|
|
start_time: string;
|
|
end_time: string;
|
|
status: string;
|
|
topic_category?: string;
|
|
meet_link?: string;
|
|
}>;
|
|
const isConsultingOrder = consultingSessions && consultingSessions.length > 0;
|
|
|
|
console.log("[HANDLE-PAID] isConsultingOrder:", isConsultingOrder, "consultingSessions:", consultingSessions);
|
|
|
|
if (isConsultingOrder) {
|
|
console.log("[HANDLE-PAID] Consulting order detected, processing sessions");
|
|
|
|
// Update consulting sessions status from pending_payment to confirmed
|
|
const { error: updateError } = await supabase
|
|
.from("consulting_sessions")
|
|
.update({ status: "confirmed" })
|
|
.eq("order_id", order_id)
|
|
.in("status", ["pending_payment"]);
|
|
|
|
console.log("[HANDLE-PAID] Session update result:", { updateError, order_id });
|
|
|
|
if (updateError) {
|
|
console.error("[HANDLE-PAID] Failed to update sessions:", updateError);
|
|
}
|
|
|
|
if (consultingSessions && consultingSessions.length > 0) {
|
|
try {
|
|
console.log("[HANDLE-PAID] Creating Google Meet for order:", order_id);
|
|
|
|
// Use the first session for Meet creation
|
|
const session = consultingSessions[0];
|
|
const topic = session.topic_category || "Konsultasi 1-on-1";
|
|
|
|
console.log("[HANDLE-PAID] Session time:", `${session.start_time} - ${session.end_time}`);
|
|
|
|
const meetResponse = await fetch(
|
|
`${supabaseUrl}/functions/v1/create-google-meet-event`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
|
|
},
|
|
body: JSON.stringify({
|
|
slot_id: session.id,
|
|
date: session.session_date,
|
|
start_time: session.start_time,
|
|
end_time: session.end_time,
|
|
client_name: userName,
|
|
client_email: userEmail,
|
|
topic: topic,
|
|
notes: `Session ID: ${session.id}`,
|
|
}),
|
|
}
|
|
);
|
|
|
|
console.log("[HANDLE-PAID] Meet response status:", meetResponse.status);
|
|
|
|
if (meetResponse.ok) {
|
|
const meetData = await meetResponse.json();
|
|
console.log("[HANDLE-PAID] Meet response data:", meetData);
|
|
|
|
if (meetData.success) {
|
|
console.log("[HANDLE-PAID] Meet created:", meetData.meet_link);
|
|
|
|
// Update session with meet link
|
|
const { error: updateError } = await supabase
|
|
.from("consulting_sessions")
|
|
.update({ meet_link: meetData.meet_link })
|
|
.eq("order_id", order_id);
|
|
|
|
if (updateError) {
|
|
console.error("[HANDLE-PAID] Failed to update meet_link:", updateError);
|
|
} else {
|
|
console.log("[HANDLE-PAID] Meet link updated for session:", order_id);
|
|
}
|
|
} else {
|
|
console.error("[HANDLE-PAID] Meet creation returned success=false:", meetData);
|
|
}
|
|
} else {
|
|
const errorText = await meetResponse.text();
|
|
console.error("[HANDLE-PAID] Meet creation failed with status:", meetResponse.status);
|
|
console.error("[HANDLE-PAID] Error response:", errorText);
|
|
}
|
|
} catch (error) {
|
|
console.error("[HANDLE-PAID] Meet creation exception:", error);
|
|
// Don't fail the entire process
|
|
}
|
|
}
|
|
|
|
// Send consulting notification with the consultingSessions data
|
|
await sendNotification(supabase, "consulting_scheduled", {
|
|
nama: userName,
|
|
email: userEmail,
|
|
order_id_short: order_id.substring(0, 8),
|
|
tanggal_pesanan: new Date().toLocaleDateString("id-ID"),
|
|
total: `Rp ${order.total_amount.toLocaleString("id-ID")}`,
|
|
metode_pembayaran: order.payment_method || "Unknown",
|
|
tanggal_konsultasi: consultingSessions[0]?.session_date || "",
|
|
jam_konsultasi: consultingSessions.map(s => `${s.start_time.substring(0, 5)} - ${s.end_time.substring(0, 5)}`).join(", "),
|
|
link_meet: consultingSessions[0]?.meet_link || "Akan dikirim terpisah",
|
|
event: "consulting_scheduled",
|
|
order_id,
|
|
user_id: order.user_id,
|
|
user_name: userName,
|
|
slots: consultingSessions,
|
|
});
|
|
} else {
|
|
// Regular product order - grant access
|
|
console.log("[HANDLE-PAID] Regular product order, granting access");
|
|
|
|
for (const item of orderItems) {
|
|
// Check if access already exists
|
|
const { data: existingAccess } = await supabase
|
|
.from("user_access")
|
|
.select("id")
|
|
.eq("user_id", order.user_id)
|
|
.eq("product_id", item.product_id)
|
|
.maybeSingle();
|
|
|
|
if (!existingAccess) {
|
|
await supabase
|
|
.from("user_access")
|
|
.insert({
|
|
user_id: order.user_id,
|
|
product_id: item.product_id,
|
|
});
|
|
console.log("[HANDLE-PAID] Access granted for product:", item.product_id);
|
|
}
|
|
}
|
|
|
|
const productTitles = orderItems.map(i => i.product.title);
|
|
|
|
// Send payment success notification
|
|
await sendNotification(supabase, "payment_success", {
|
|
nama: userName,
|
|
email: userEmail,
|
|
order_id_short: order_id.substring(0, 8),
|
|
tanggal_pesanan: new Date().toLocaleDateString("id-ID"),
|
|
total: `Rp ${order.total_amount.toLocaleString("id-ID")}`,
|
|
metode_pembayaran: order.payment_method || "Unknown",
|
|
produk: productTitles.join(", "),
|
|
link_akses: `${Deno.env.get("SITE_URL") || ""}/access`,
|
|
event: "payment_success",
|
|
order_id,
|
|
user_id: order.user_id,
|
|
user_name: userName,
|
|
products: productTitles,
|
|
});
|
|
|
|
// Send access granted notification
|
|
await sendNotification(supabase, "access_granted", {
|
|
nama: userName,
|
|
email: userEmail,
|
|
produk: productTitles.join(", "),
|
|
event: "access_granted",
|
|
order_id,
|
|
user_id: order.user_id,
|
|
user_name: userName,
|
|
products: productTitles,
|
|
});
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, order_id }),
|
|
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
|
|
} catch (error: any) {
|
|
console.error("[HANDLE-PAID] Error:", error);
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
error: error.message || "Internal server error"
|
|
}),
|
|
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
});
|
|
|
|
// Helper function to send notification
|
|
async function sendNotification(
|
|
supabase: any,
|
|
templateKey: string,
|
|
data: Record<string, any>
|
|
): Promise<void> {
|
|
console.log("[HANDLE-PAID] Sending notification:", templateKey);
|
|
|
|
// Fetch template
|
|
const { data: template } = await supabase
|
|
.from("notification_templates")
|
|
.select("*")
|
|
.eq("key", templateKey)
|
|
.single();
|
|
|
|
if (!template) {
|
|
console.log("[HANDLE-PAID] Template not found:", templateKey);
|
|
return;
|
|
}
|
|
|
|
// Send webhook if configured
|
|
if (template.webhook_url) {
|
|
try {
|
|
await fetch(template.webhook_url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
console.log("[HANDLE-PAID] Webhook sent to:", template.webhook_url);
|
|
} catch (error) {
|
|
console.error("[HANDLE-PAID] Webhook failed:", error);
|
|
}
|
|
}
|
|
|
|
// Skip email if template is inactive
|
|
if (!template.is_active) {
|
|
console.log("[HANDLE-PAID] Template inactive, skipping email");
|
|
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,
|
|
}),
|
|
});
|
|
}
|