Files

428 lines
15 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 (
id,
product_id,
unit_price,
product:products (title, type, collaborator_user_id, profit_share_percentage, auto_grant_access)
),
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<{
id: string;
product_id: string;
unit_price?: number;
product: {
title: string;
type: string;
collaborator_user_id?: string | null;
profit_share_percentage?: number | null;
auto_grant_access?: boolean | null;
};
}>;
// 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);
}
// Collaboration: credit collaborator wallet if this product has a collaborator
const collaboratorUserId = item.product?.collaborator_user_id;
const profitSharePct = Number(item.product?.profit_share_percentage || 0);
const autoGrantAccess = item.product?.auto_grant_access !== false;
const itemPrice = Number(item.unit_price || 0);
if (collaboratorUserId && profitSharePct > 0 && itemPrice > 0) {
const hostShare = itemPrice * ((100 - profitSharePct) / 100);
const collaboratorShare = itemPrice * (profitSharePct / 100);
// Save profit split to order_items
const { error: splitError } = await supabase
.from("order_items")
.update({
host_share: hostShare,
collaborator_share: collaboratorShare,
})
.eq("id", item.id);
if (splitError) {
console.error("[HANDLE-PAID] Failed to update order item split:", splitError);
continue;
}
// Credit collaborator wallet (also stores wallet_transaction_id on order_items)
const { data: transactionId, error: creditError } = await supabase
.rpc("credit_collaborator_wallet", {
p_user_id: collaboratorUserId,
p_order_item_id: item.id,
p_amount: collaboratorShare,
p_description: `Profit from sale: ${item.product?.title || "Product"}`,
});
if (creditError) {
console.error("[HANDLE-PAID] Failed to credit collaborator wallet:", creditError);
continue;
}
console.log(
`[HANDLE-PAID] Credited collaborator wallet: ${collaboratorUserId} + Rp ${collaboratorShare}, tx=${transactionId}`
);
// Grant collaborator access to the same product if enabled
if (autoGrantAccess) {
const { error: collaboratorAccessError } = await supabase
.from("user_access")
.upsert(
{
user_id: collaboratorUserId,
product_id: item.product_id,
access_type: "collaborator",
granted_by: order.user_id,
},
{ onConflict: "user_id,product_id" }
);
if (collaboratorAccessError) {
console.error("[HANDLE-PAID] Failed to grant collaborator access:", collaboratorAccessError);
}
}
// Notify collaborator about new sale
const { error: collabNotifyError } = await supabase.functions.invoke("send-collaboration-notification", {
body: {
type: "new_sale",
collaboratorUserId,
productTitle: item.product?.title || "Product",
profitAmount: collaboratorShare,
profitSharePercentage: profitSharePct,
saleDate: order.created_at,
},
});
if (collabNotifyError) {
console.error("[HANDLE-PAID] Failed to send collaborator notification:", collabNotifyError);
}
}
}
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: unknown) {
console.error("[HANDLE-PAID] Error:", error);
const message = error instanceof Error ? error.message : "Internal server error";
return new Response(
JSON.stringify({
success: false,
error: message
}),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
// Helper function to send notification
async function sendNotification(
supabase: ReturnType<typeof createClient>,
templateKey: string,
data: Record<string, unknown>
): 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 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: String(data.email || ""),
recipient_name: String((data.user_name as string) || (data.nama as string) || ""),
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);
}
}