From 3eb53406c99d918c416bc4b1e70d7c96b6d5a6d5 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 28 Dec 2025 18:13:20 +0700 Subject: [PATCH] Auto-cancel expired consulting orders and prefill re-booking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Features Implemented:** 1. **Auto-Cancel Expired Consulting Orders:** - New edge function: cancel-expired-consulting-orders - Changes order status from 'pending' → 'cancelled' - Cancels all associated consulting_sessions - Deletes calendar events via delete-calendar-event - Releases consulting_time_slots (deletes booked slots) - Properly cleans up all resources 2. **Smart Re-Booking with Pre-filled Data:** - OrderDetail.tsx stores expired order data in sessionStorage: - fromExpiredOrder flag - Original orderId - topicCategory - notes - ConsultingBooking.tsx retrieves and pre-fills form on mount - Auto-clears sessionStorage after use 3. **Improved UX for Expired Orders:** - Clear message: "Order ini telah dibatalkan secara otomatis" - Helpful hint: "Kategori dan catatan akan terisi otomatis" - One-click re-booking with pre-filled data - Member only needs to select new time slot **How It Works:** Flow: 1. QRIS expires → Order shows expired message 2. Member clicks "Buat Booking Baru" 3. Data stored in sessionStorage (category, notes) 4. Navigates to /consulting 5. Form auto-fills with previous data 6. Member selects new time → Books new session **Edge Function Details:** - Finds orders where: payment_status='pending' AND qr_expires_at < NOW() - Cancels order status - Cancels consulting_sessions - Deletes consulting_time_slots - Invokes delete-calendar-event for each session - Returns count of processed orders **To Deploy:** 1. Deploy cancel-expired-consulting-orders edge function 2. Set up cron job to run every 5-15 minutes: `curl -X POST https://your-domain/functions/v1/cancel-expired-consulting-orders` **Benefits:** ✅ Orders properly cancelled when QR expires ✅ Time slots released for other users ✅ Calendar events cleaned up ✅ Easy re-booking without re-typing data ✅ Better UX for expired payment situations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/pages/ConsultingBooking.tsx | 25 ++++ src/pages/member/OrderDetail.tsx | 21 ++- .../cancel-expired-consulting-orders/index.ts | 136 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 supabase/functions/cancel-expired-consulting-orders/index.ts diff --git a/src/pages/ConsultingBooking.tsx b/src/pages/ConsultingBooking.tsx index 4929ac4..6f6d510 100644 --- a/src/pages/ConsultingBooking.tsx +++ b/src/pages/ConsultingBooking.tsx @@ -82,6 +82,31 @@ export default function ConsultingBooking() { useEffect(() => { fetchData(); + + // Check for pre-filled data from expired order + const expiredOrderData = sessionStorage.getItem('expiredConsultingOrder'); + if (expiredOrderData) { + try { + const data = JSON.parse(expiredOrderData); + if (data.fromExpiredOrder) { + // Prefill form with expired order data + if (data.topicCategory) setSelectedCategory(data.topicCategory); + if (data.notes) setNotes(data.notes); + + // Show notification to user + setTimeout(() => { + // You could add a toast notification here if you have toast set up + console.log('Pre-filled data from expired order:', data); + }, 100); + + // Clear the stored data after using it + sessionStorage.removeItem('expiredConsultingOrder'); + } + } catch (err) { + console.error('Error parsing expired order data:', err); + sessionStorage.removeItem('expiredConsultingOrder'); + } + } }, []); useEffect(() => { diff --git a/src/pages/member/OrderDetail.tsx b/src/pages/member/OrderDetail.tsx index 71cd94b..91dea74 100644 --- a/src/pages/member/OrderDetail.tsx +++ b/src/pages/member/OrderDetail.tsx @@ -449,15 +449,32 @@ export default function OrderDetail() { {isConsultingOrder ? ( - // Consulting order - show booking button + // Consulting order - show booking button with pre-filled data

Order ini telah dibatalkan secara otomatis karena waktu pembayaran habis.

- +

+ Kategori dan catatan akan terisi otomatis dari order sebelumnya +

) : ( // Product order - show regenerate button diff --git a/supabase/functions/cancel-expired-consulting-orders/index.ts b/supabase/functions/cancel-expired-consulting-orders/index.ts new file mode 100644 index 0000000..eba264f --- /dev/null +++ b/supabase/functions/cancel-expired-consulting-orders/index.ts @@ -0,0 +1,136 @@ +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 => { + 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); + + console.log("[CANCEL-EXPIRED] Starting check for expired consulting orders"); + + // Find expired pending consulting orders + const now = new Date().toISOString(); + + // Get orders with consulting_sessions that are pending payment and QR is expired + const { data: expiredOrders, error: queryError } = await supabase + .from("orders") + .select(` + id, + payment_status, + qr_expires_at, + consulting_sessions ( + id, + topic_category, + notes, + session_date, + start_time, + end_time + ) + `) + .eq("payment_status", "pending") + .lt("qr_expires_at", now) + .not("consulting_sessions", "is", null); + + if (queryError) { + console.error("[CANCEL-EXPIRED] Query error:", queryError); + throw queryError; + } + + if (!expiredOrders || expiredOrders.length === 0) { + console.log("[CANCEL-EXPIRED] No expired orders found"); + return new Response( + JSON.stringify({ + success: true, + message: "No expired orders to process", + processed: 0 + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log(`[CANCEL-EXPIRED] Found ${expiredOrders.length} expired orders`); + + let processedCount = 0; + + // Process each expired order + for (const order of expiredOrders) { + console.log(`[CANCEL-EXPIRED] Processing order: ${order.id}`); + + // Update order status to cancelled + const { error: updateError } = await supabase + .from("orders") + .update({ status: "cancelled" }) + .eq("id", order.id); + + if (updateError) { + console.error(`[CANCEL-EXPIRED] Failed to update order ${order.id}:`, updateError); + continue; + } + + // Cancel all consulting sessions for this order + if (order.consulting_sessions && order.consulting_sessions.length > 0) { + for (const session of order.consulting_sessions) { + // Delete calendar event if exists + if (session.calendar_event_id) { + try { + await supabase.functions.invoke('delete-calendar-event', { + body: { session_id: session.id } + }); + console.log(`[CANCEL-EXPIRED] Deleted calendar event for session: ${session.id}`); + } catch (err) { + console.log(`[CANCEL-EXPIRED] Failed to delete calendar event: ${err}`); + // Continue anyway + } + } + + // Update session status to cancelled + await supabase + .from("consulting_sessions") + .update({ status: "cancelled" }) + .eq("id", session.id); + + // Delete or release time slots + await supabase + .from("consulting_time_slots") + .delete() + .eq("session_id", session.id); + + console.log(`[CANCEL-EXPIRED] Cancelled session: ${session.id}`); + } + } + + processedCount++; + } + + console.log(`[CANCEL-EXPIRED] Successfully processed ${processedCount} orders`); + + return new Response( + JSON.stringify({ + success: true, + message: `Successfully cancelled ${processedCount} expired consulting orders`, + processed: processedCount + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + + } catch (error: any) { + console.error("[CANCEL-EXPIRED] Error:", error); + return new Response( + JSON.stringify({ + success: false, + error: error.message || "Internal server error" + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +});