Add calendar event lifecycle management and "Add to Calendar" feature
- 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>
This commit is contained in:
@@ -311,27 +311,15 @@ serve(async (req: Request): Promise<Response> => {
|
||||
if (meetLink) {
|
||||
log(`Meet link found: ${meetLink}`);
|
||||
|
||||
// If this is part of a multi-slot order, update all slots with the same order_id
|
||||
// First, check if this slot has an order_id
|
||||
const { data: slotData } = await supabase
|
||||
.from("consulting_slots")
|
||||
.select("order_id")
|
||||
.eq("id", body.slot_id)
|
||||
.single();
|
||||
|
||||
if (slotData?.order_id) {
|
||||
log(`Updating all slots in order ${slotData.order_id} with meet_link`);
|
||||
await supabase
|
||||
.from("consulting_slots")
|
||||
.update({ meet_link: meetLink })
|
||||
.eq("order_id", slotData.order_id);
|
||||
} else {
|
||||
log(`No order_id found, updating only slot ${body.slot_id}`);
|
||||
await supabase
|
||||
.from("consulting_slots")
|
||||
.update({ meet_link: meetLink })
|
||||
.eq("id", body.slot_id);
|
||||
}
|
||||
// Update consulting_sessions with meet_link and event_id
|
||||
log(`Updating session ${body.slot_id} with meet_link and calendar_event_id`);
|
||||
await supabase
|
||||
.from("consulting_sessions")
|
||||
.update({
|
||||
meet_link: meetLink,
|
||||
calendar_event_id: eventDataResult.id
|
||||
})
|
||||
.eq("id", body.slot_id);
|
||||
|
||||
log("Successfully completed");
|
||||
return new Response(
|
||||
@@ -351,26 +339,15 @@ serve(async (req: Request): Promise<Response> => {
|
||||
if (eventDataResult.hangoutLink) {
|
||||
log(`Using hangoutLink: ${eventDataResult.hangoutLink}`);
|
||||
|
||||
// If this is part of a multi-slot order, update all slots with the same order_id
|
||||
const { data: slotData } = await supabase
|
||||
.from("consulting_slots")
|
||||
.select("order_id")
|
||||
.eq("id", body.slot_id)
|
||||
.single();
|
||||
|
||||
if (slotData?.order_id) {
|
||||
log(`Updating all slots in order ${slotData.order_id} with meet_link`);
|
||||
await supabase
|
||||
.from("consulting_slots")
|
||||
.update({ meet_link: eventDataResult.hangoutLink })
|
||||
.eq("order_id", slotData.order_id);
|
||||
} else {
|
||||
log(`No order_id found, updating only slot ${body.slot_id}`);
|
||||
await supabase
|
||||
.from("consulting_slots")
|
||||
.update({ meet_link: eventDataResult.hangoutLink })
|
||||
.eq("id", body.slot_id);
|
||||
}
|
||||
// Update consulting_sessions with meet_link and event_id
|
||||
log(`Updating session ${body.slot_id} with meet_link and calendar_event_id`);
|
||||
await supabase
|
||||
.from("consulting_sessions")
|
||||
.update({
|
||||
meet_link: eventDataResult.hangoutLink,
|
||||
calendar_event_id: eventDataResult.id
|
||||
})
|
||||
.eq("id", body.slot_id);
|
||||
|
||||
log("Successfully completed");
|
||||
return new Response(
|
||||
|
||||
@@ -1,132 +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",
|
||||
};
|
||||
|
||||
interface CreateMeetRequest {
|
||||
slot_id: string;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
client_name: string;
|
||||
client_email: string;
|
||||
topic: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const body: CreateMeetRequest = await req.json();
|
||||
console.log("Creating meet link for slot:", body.slot_id);
|
||||
|
||||
// Get platform settings for Google Calendar ID
|
||||
const { data: settings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("integration_google_calendar_id, brand_name")
|
||||
.single();
|
||||
|
||||
const calendarId = settings?.integration_google_calendar_id;
|
||||
const brandName = settings?.brand_name || "LearnHub";
|
||||
|
||||
if (!calendarId) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "Google Calendar ID belum dikonfigurasi di Pengaturan > Integrasi"
|
||||
}),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// For now, this is a placeholder that returns a message
|
||||
// In production, you would integrate with Google Calendar API via OAuth or service account
|
||||
// Or call an n8n webhook to handle the calendar creation
|
||||
|
||||
const { data: integrationSettings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("integration_n8n_base_url, integration_n8n_test_mode")
|
||||
.single();
|
||||
|
||||
if (integrationSettings?.integration_n8n_base_url) {
|
||||
// Check if we're in test mode (controlled by the integration_n8n_test_mode setting)
|
||||
const isTestMode = integrationSettings.integration_n8n_test_mode || false;
|
||||
|
||||
const webhookPath = isTestMode ? "/webhook-test/" : "/webhook/";
|
||||
const n8nUrl = `${integrationSettings.integration_n8n_base_url}${webhookPath}create-meet`;
|
||||
|
||||
console.log(`Calling n8n webhook: ${n8nUrl} (Test mode: ${isTestMode})`);
|
||||
|
||||
try {
|
||||
const n8nResponse = await fetch(n8nUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
slot_id: body.slot_id,
|
||||
date: body.date,
|
||||
start_time: body.start_time,
|
||||
end_time: body.end_time,
|
||||
client_name: body.client_name,
|
||||
client_email: body.client_email,
|
||||
topic: body.topic,
|
||||
notes: body.notes,
|
||||
calendar_id: calendarId,
|
||||
brand_name: brandName,
|
||||
test_mode: isTestMode, // Add test_mode flag for n8n to use
|
||||
}),
|
||||
});
|
||||
|
||||
if (n8nResponse.ok) {
|
||||
const result = await n8nResponse.json();
|
||||
|
||||
if (result.meet_link) {
|
||||
// Update the slot with the meet link
|
||||
await supabase
|
||||
.from("consulting_slots")
|
||||
.update({ meet_link: result.meet_link })
|
||||
.eq("id", body.slot_id);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, meet_link: result.meet_link }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (n8nError) {
|
||||
console.error("n8n webhook error:", n8nError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Return instructions for manual setup
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "Integrasi otomatis belum tersedia. Silakan buat link Meet secara manual atau konfigurasi n8n webhook di Pengaturan > Integrasi.",
|
||||
manual_instructions: {
|
||||
calendar_id: calendarId,
|
||||
event_title: `Konsultasi: ${body.topic} - ${body.client_name}`,
|
||||
event_date: body.date,
|
||||
event_time: `${body.start_time} - ${body.end_time}`,
|
||||
}
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error creating meet link:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, message: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
193
supabase/functions/delete-calendar-event/index.ts
Normal file
193
supabase/functions/delete-calendar-event/index.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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 GoogleOAuthConfig {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
refresh_token: string;
|
||||
access_token?: string;
|
||||
expires_at?: number;
|
||||
}
|
||||
|
||||
interface DeleteEventRequest {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
// Function to get access token from refresh token (OAuth2)
|
||||
async function getGoogleAccessToken(oauthConfig: GoogleOAuthConfig): Promise<{ access_token: string; expires_in: number }> {
|
||||
try {
|
||||
console.log("Refreshing access token for calendar event deletion...");
|
||||
|
||||
const tokenRequest = {
|
||||
client_id: oauthConfig.client_id,
|
||||
client_secret: oauthConfig.client_secret,
|
||||
refresh_token: oauthConfig.refresh_token,
|
||||
grant_type: "refresh_token",
|
||||
};
|
||||
|
||||
const response = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(tokenRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Token exchange failed: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.access_token) {
|
||||
throw new Error("No access token in response");
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: data.access_token,
|
||||
expires_in: data.expires_in || 3600
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("Error getting Google access token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const body: DeleteEventRequest = await req.json();
|
||||
console.log("[DELETE-CALENDAR-EVENT] Deleting event for session:", body.session_id);
|
||||
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Get session data with calendar_event_id
|
||||
const { data: session, error: sessionError } = await supabase
|
||||
.from("consulting_sessions")
|
||||
.select("id, calendar_event_id, user_id")
|
||||
.eq("id", body.session_id)
|
||||
.single();
|
||||
|
||||
if (sessionError || !session) {
|
||||
console.error("[DELETE-CALENDAR-EVENT] Session not found:", sessionError);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: "Session not found" }),
|
||||
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.calendar_event_id) {
|
||||
console.log("[DELETE-CALENDAR-EVENT] No calendar_event_id found, skipping deletion");
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "No calendar event to delete" }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get OAuth config
|
||||
const { data: settings } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("integration_google_calendar_id, google_oauth_config")
|
||||
.single();
|
||||
|
||||
const calendarId = settings?.integration_google_calendar_id;
|
||||
const oauthConfigJson = settings?.google_oauth_config;
|
||||
|
||||
if (!calendarId || !oauthConfigJson) {
|
||||
console.log("[DELETE-CALENDAR-EVENT] Calendar not configured, skipping deletion");
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "Calendar not configured" }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Parse OAuth config
|
||||
let oauthConfig: GoogleOAuthConfig;
|
||||
try {
|
||||
oauthConfig = JSON.parse(oauthConfigJson);
|
||||
} catch (error) {
|
||||
console.error("[DELETE-CALENDAR-EVENT] Failed to parse OAuth config");
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: "Invalid OAuth config" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get access token
|
||||
let accessToken: string;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (oauthConfig.access_token && oauthConfig.expires_at && oauthConfig.expires_at > now + 60) {
|
||||
accessToken = oauthConfig.access_token;
|
||||
} else {
|
||||
const tokenData = await getGoogleAccessToken(oauthConfig);
|
||||
accessToken = tokenData.access_token;
|
||||
|
||||
// Update cached token
|
||||
const newExpiresAt = now + tokenData.expires_in;
|
||||
const updatedConfig = {
|
||||
...oauthConfig,
|
||||
access_token: accessToken,
|
||||
expires_at: newExpiresAt
|
||||
};
|
||||
|
||||
await supabase
|
||||
.from("platform_settings")
|
||||
.update({ google_oauth_config: JSON.stringify(updatedConfig) })
|
||||
.eq("id", settings.id);
|
||||
}
|
||||
|
||||
// Delete event from Google Calendar
|
||||
console.log(`[DELETE-CALENDAR-EVENT] Deleting event ${session.calendar_event_id} from calendar ${calendarId}`);
|
||||
|
||||
const deleteResponse = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(session.calendar_event_id)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!deleteResponse.ok) {
|
||||
if (deleteResponse.status === 410) {
|
||||
// Event already deleted (Gone)
|
||||
console.log("[DELETE-CALENDAR-EVENT] Event already deleted (410)");
|
||||
} else {
|
||||
const errorText = await deleteResponse.text();
|
||||
console.error("[DELETE-CALENDAR-EVENT] Failed to delete event:", errorText);
|
||||
// Don't fail the operation, just log it
|
||||
}
|
||||
} else {
|
||||
console.log("[DELETE-CALENDAR-EVENT] Event deleted successfully");
|
||||
}
|
||||
|
||||
// Clear calendar_event_id from session
|
||||
await supabase
|
||||
.from("consulting_sessions")
|
||||
.update({ calendar_event_id: null })
|
||||
.eq("id", body.session_id);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "Calendar event deleted" }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[DELETE-CALENDAR-EVENT] Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -30,7 +30,7 @@ serve(async (req: Request): Promise<Response> => {
|
||||
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Get full order details with items AND consulting slots
|
||||
// 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")
|
||||
@@ -41,12 +41,13 @@ serve(async (req: Request): Promise<Response> => {
|
||||
product_id,
|
||||
product:products (title, type)
|
||||
),
|
||||
consulting_slots (
|
||||
consulting_sessions (
|
||||
id,
|
||||
date,
|
||||
session_date,
|
||||
start_time,
|
||||
end_time,
|
||||
status
|
||||
status,
|
||||
topic_category
|
||||
)
|
||||
`)
|
||||
.eq("id", order_id)
|
||||
@@ -72,8 +73,8 @@ serve(async (req: Request): Promise<Response> => {
|
||||
id: order.id,
|
||||
payment_status: order.payment_status,
|
||||
order_items_count: order.order_items?.length || 0,
|
||||
consulting_slots_count: order.consulting_slots?.length || 0,
|
||||
consulting_slots: order.consulting_slots
|
||||
consulting_sessions_count: order.consulting_sessions?.length || 0,
|
||||
consulting_sessions: order.consulting_sessions
|
||||
}));
|
||||
|
||||
const userEmail = order.profiles?.email || "";
|
||||
@@ -83,49 +84,45 @@ serve(async (req: Request): Promise<Response> => {
|
||||
product: { title: string; type: string };
|
||||
}>;
|
||||
|
||||
// Check if this is a consulting order by checking consulting_slots
|
||||
const consultingSlots = order.consulting_slots as Array<{
|
||||
// Check if this is a consulting order by checking consulting_sessions
|
||||
const consultingSessions = order.consulting_sessions as Array<{
|
||||
id: string;
|
||||
date: string;
|
||||
session_date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
status: string;
|
||||
topic_category?: string;
|
||||
meet_link?: string;
|
||||
}>;
|
||||
const isConsultingOrder = consultingSlots && consultingSlots.length > 0;
|
||||
const isConsultingOrder = consultingSessions && consultingSessions.length > 0;
|
||||
|
||||
console.log("[HANDLE-PAID] isConsultingOrder:", isConsultingOrder, "consultingSlots:", consultingSlots);
|
||||
console.log("[HANDLE-PAID] isConsultingOrder:", isConsultingOrder, "consultingSessions:", consultingSessions);
|
||||
|
||||
if (isConsultingOrder) {
|
||||
console.log("[HANDLE-PAID] Consulting order detected, processing slots");
|
||||
console.log("[HANDLE-PAID] Consulting order detected, processing sessions");
|
||||
|
||||
// Sort slots by start_time to ensure correct ordering
|
||||
consultingSlots.sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||
|
||||
// Update consulting slots status from pending_payment to confirmed
|
||||
// Update consulting sessions status from pending_payment to confirmed
|
||||
const { error: updateError } = await supabase
|
||||
.from("consulting_slots")
|
||||
.from("consulting_sessions")
|
||||
.update({ status: "confirmed" })
|
||||
.eq("order_id", order_id)
|
||||
.in("status", ["pending_payment"]);
|
||||
|
||||
console.log("[HANDLE-PAID] Slot update result:", { updateError, order_id });
|
||||
console.log("[HANDLE-PAID] Session update result:", { updateError, order_id });
|
||||
|
||||
if (updateError) {
|
||||
console.error("[HANDLE-PAID] Failed to update slots:", updateError);
|
||||
console.error("[HANDLE-PAID] Failed to update sessions:", updateError);
|
||||
}
|
||||
|
||||
if (consultingSlots && consultingSlots.length > 0) {
|
||||
if (consultingSessions && consultingSessions.length > 0) {
|
||||
try {
|
||||
console.log("[HANDLE-PAID] Creating Google Meet for order:", order_id);
|
||||
|
||||
// Group slots by order - use first slot's start time and last slot's end time
|
||||
const firstSlot = consultingSlots[0];
|
||||
const lastSlot = consultingSlots[consultingSlots.length - 1];
|
||||
const topic = "Konsultasi 1-on-1";
|
||||
// Use the first session for Meet creation
|
||||
const session = consultingSessions[0];
|
||||
const topic = session.topic_category || "Konsultasi 1-on-1";
|
||||
|
||||
console.log("[HANDLE-PAID] Time slots:", consultingSlots.map(s => `${s.start_time}-${s.end_time}`).join(', '));
|
||||
console.log("[HANDLE-PAID] Event will be:", `${firstSlot.start_time} - ${lastSlot.end_time}`);
|
||||
console.log("[HANDLE-PAID] Session time:", `${session.start_time} - ${session.end_time}`);
|
||||
|
||||
const meetResponse = await fetch(
|
||||
`${supabaseUrl}/functions/v1/create-google-meet-event`,
|
||||
@@ -136,14 +133,14 @@ serve(async (req: Request): Promise<Response> => {
|
||||
"Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
slot_id: firstSlot.id, // Use first slot ID
|
||||
date: firstSlot.date,
|
||||
start_time: firstSlot.start_time,
|
||||
end_time: lastSlot.end_time, // Use last slot's end time for continuous block
|
||||
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: `${consultingSlots.length} sesi: ${consultingSlots.map(s => s.start_time.substring(0, 5)).join(', ')}`,
|
||||
notes: `Session ID: ${session.id}`,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -157,16 +154,16 @@ serve(async (req: Request): Promise<Response> => {
|
||||
if (meetData.success) {
|
||||
console.log("[HANDLE-PAID] Meet created:", meetData.meet_link);
|
||||
|
||||
// Update all slots with the same meet link
|
||||
// Update session with meet link
|
||||
const { error: updateError } = await supabase
|
||||
.from("consulting_slots")
|
||||
.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 all slots in order:", order_id);
|
||||
console.log("[HANDLE-PAID] Meet link updated for session:", order_id);
|
||||
}
|
||||
} else {
|
||||
console.error("[HANDLE-PAID] Meet creation returned success=false:", meetData);
|
||||
@@ -182,7 +179,7 @@ serve(async (req: Request): Promise<Response> => {
|
||||
}
|
||||
}
|
||||
|
||||
// Send consulting notification with the consultingSlots data
|
||||
// Send consulting notification with the consultingSessions data
|
||||
await sendNotification(supabase, "consulting_scheduled", {
|
||||
nama: userName,
|
||||
email: userEmail,
|
||||
@@ -190,14 +187,14 @@ serve(async (req: Request): Promise<Response> => {
|
||||
tanggal_pesanan: new Date().toLocaleDateString("id-ID"),
|
||||
total: `Rp ${order.total_amount.toLocaleString("id-ID")}`,
|
||||
metode_pembayaran: order.payment_method || "Unknown",
|
||||
tanggal_konsultasi: consultingSlots[0]?.date || "",
|
||||
jam_konsultasi: consultingSlots.map(s => s.start_time.substring(0, 5)).join(", "),
|
||||
link_meet: consultingSlots[0]?.meet_link || "Akan dikirim terpisah",
|
||||
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: consultingSlots,
|
||||
slots: consultingSessions,
|
||||
});
|
||||
} else {
|
||||
// Regular product order - grant access
|
||||
|
||||
12
supabase/migrations/20241228_add_calendar_event_id.sql
Normal file
12
supabase/migrations/20241228_add_calendar_event_id.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Add calendar_event_id column to consulting_sessions
|
||||
-- This stores the Google Calendar event ID for later deletion
|
||||
|
||||
ALTER TABLE consulting_sessions
|
||||
ADD COLUMN calendar_event_id TEXT;
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX idx_consulting_sessions_calendar_event
|
||||
ON consulting_sessions(calendar_event_id);
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON COLUMN consulting_sessions.calendar_event_id IS 'Google Calendar event ID - used to delete events when sessions are cancelled/refunded';
|
||||
Reference in New Issue
Block a user