From 1a36f831ccd15363d566c21b0a8aa26682f1702b Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 23 Dec 2025 21:41:47 +0700 Subject: [PATCH] Refactor: Rename create-pakasir-payment to create-payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename function to abstract payment provider details - Add support for both QRIS and PayPal methods - Update frontend to use generic create-payment function - Remove provider-specific naming from UI/UX - Payment provider (Pakasir) is now an implementation detail Response format: - QRIS: returns qr_string for in-app display, payment_url as fallback - PayPal: returns payment_url for redirect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/pages/Checkout.tsx | 42 +++--- src/pages/ConsultingBooking.tsx | 7 +- supabase/config.toml | 2 +- .../functions/create-pakasir-payment/index.ts | 102 -------------- supabase/functions/create-payment/index.ts | 131 ++++++++++++++++++ 5 files changed, 158 insertions(+), 126 deletions(-) delete mode 100644 supabase/functions/create-pakasir-payment/index.ts create mode 100644 supabase/functions/create-payment/index.ts diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx index 35cc9bd..d74ddfd 100644 --- a/src/pages/Checkout.tsx +++ b/src/pages/Checkout.tsx @@ -132,24 +132,28 @@ export default function Checkout() { // Build description from product titles const productTitles = items.map(item => item.title).join(", "); - if (paymentMethod === "qris") { - // Call edge function to create Pakasir QRIS payment (avoids CORS) - const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-pakasir-payment', { - body: { - order_id: order.id, - amount: amountInRupiah, - description: productTitles, - }, - }); + // Call edge function to create payment (avoids CORS) + const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', { + body: { + order_id: order.id, + amount: amountInRupiah, + description: productTitles, + method: paymentMethod, // 'qris' or 'paypal' + }, + }); - if (paymentError) { - console.error('Payment creation error:', paymentError); - // Fallback to redirect - const pakasirUrl = `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${amountInRupiah}?order_id=${order.id}`; + if (paymentError) { + console.error('Payment creation error:', paymentError); + throw new Error(paymentError.message || 'Gagal membuat pembayaran'); + } + + if (paymentData?.success) { + if (paymentData.data.method === 'paypal') { + // PayPal - redirect to payment URL clearCart(); - window.location.href = pakasirUrl; - } else if (paymentData?.data?.qr_string) { - // QRIS available - show QR code + window.location.href = paymentData.data.payment_url; + } else if (paymentData.data.qr_string) { + // QRIS available - show QR code in app setPaymentData({ qr_string: paymentData.data.qr_string, expired_at: paymentData.data.expired_at, @@ -159,13 +163,11 @@ export default function Checkout() { clearCart(); } else { // No QR code - redirect to payment page + clearCart(); window.location.href = paymentData.data.payment_url; } } else { - // PayPal - redirect to Pakasir PayPal URL - clearCart(); - const paypalUrl = `https://app.pakasir.com/paypal/${PAKASIR_PROJECT_SLUG}/${amountInRupiah}?order_id=${order.id}`; - window.location.href = paypalUrl; + throw new Error('Gagal membuat pembayaran'); } } catch (error) { console.error("Checkout error:", error); diff --git a/src/pages/ConsultingBooking.tsx b/src/pages/ConsultingBooking.tsx index 202592c..a298151 100644 --- a/src/pages/ConsultingBooking.tsx +++ b/src/pages/ConsultingBooking.tsx @@ -226,12 +226,13 @@ export default function ConsultingBooking() { const { error: slotsError } = await supabase.from('consulting_slots').insert(slotsToInsert); if (slotsError) throw slotsError; - // Call edge function to create Pakasir payment (avoids CORS) - const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-pakasir-payment', { + // Call edge function to create payment (avoids CORS) + const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', { body: { order_id: order.id, amount: totalPrice, description: `Konsultasi 1-on-1 (${totalBlocks} blok)`, + method: 'qris', }, }); @@ -241,7 +242,7 @@ export default function ConsultingBooking() { } if (paymentData?.success && paymentData?.data?.payment_url) { - // Redirect to Pakasir payment page + // Redirect to payment page window.location.href = paymentData.data.payment_url; } else { throw new Error('Gagal membuat URL pembayaran'); diff --git a/supabase/config.toml b/supabase/config.toml index 82c28a7..49cc33e 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -45,5 +45,5 @@ verify_jwt = false [functions.delete-order] verify_jwt = false -[functions.create-pakasir-payment] +[functions.create-payment] verify_jwt = false diff --git a/supabase/functions/create-pakasir-payment/index.ts b/supabase/functions/create-pakasir-payment/index.ts deleted file mode 100644 index 50e5091..0000000 --- a/supabase/functions/create-pakasir-payment/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; - -const corsHeaders = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", -}; - -interface PakasirPaymentRequest { - order_id: string; - amount: number; - description: string; -} - -serve(async (req: Request) => { - // Handle CORS preflight - if (req.method === "OPTIONS") { - return new Response(null, { headers: corsHeaders }); - } - - if (req.method !== "POST") { - return new Response("Method not allowed", { status: 405, headers: corsHeaders }); - } - - try { - const body: PakasirPaymentRequest = await req.json(); - const { order_id, amount, description } = body; - - if (!order_id || !amount) { - return new Response( - JSON.stringify({ success: false, error: "order_id and amount are required" }), - { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } - - const PAKASIR_PROJECT_SLUG = Deno.env.get("PAKASIR_PROJECT_SLUG") || ""; - const PAKASIR_API_KEY = Deno.env.get("PAKASIR_API_KEY") || ""; - const PAKASIR_CALLBACK_URL = `${Deno.env.get("SUPABASE_URL")}/functions/v1/pakasir-webhook`; - - if (!PAKASIR_PROJECT_SLUG || !PAKASIR_API_KEY) { - console.error("[PAKASIR] Missing credentials"); - return new Response( - JSON.stringify({ success: false, error: "Pakasir credentials not configured" }), - { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } - - console.log("[PAKASIR] Creating payment transaction:", { order_id, amount }); - - // Call Pakasir API to create QRIS transaction - const pakasirResponse = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - project: PAKASIR_PROJECT_SLUG, - order_id: order_id, - amount: amount, - api_key: PAKASIR_API_KEY, - description: description || `Order ${order_id}`, - callback_url: PAKASIR_CALLBACK_URL, - }), - }); - - if (!pakasirResponse.ok) { - const errorText = await pakasirResponse.text(); - console.error("[PAKASIR] API error:", pakasirResponse.status, errorText); - return new Response( - JSON.stringify({ - success: false, - error: "Pakasir API error", - details: errorText - }), - { status: pakasirResponse.status, headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } - - const result = await pakasirResponse.json(); - console.log("[PAKASIR] Payment created:", result); - - // Return payment URL and QR data - return new Response( - JSON.stringify({ - success: true, - data: { - payment_url: `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${amount}?order_id=${order_id}`, - qr_string: result.qr_string || result.qr || null, - order_id: order_id, - } - }), - { headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - - } catch (error: any) { - console.error("[PAKASIR] Unexpected error:", error); - return new Response( - JSON.stringify({ - success: false, - error: error.message || "Internal server error" - }), - { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } -}); diff --git a/supabase/functions/create-payment/index.ts b/supabase/functions/create-payment/index.ts new file mode 100644 index 0000000..6a260cb --- /dev/null +++ b/supabase/functions/create-payment/index.ts @@ -0,0 +1,131 @@ +import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", +}; + +interface CreatePaymentRequest { + order_id: string; + amount: number; + description: string; + method?: 'qris' | 'paypal'; +} + +serve(async (req: Request) => { + // Handle CORS preflight + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + if (req.method !== "POST") { + return new Response("Method not allowed", { status: 405, headers: corsHeaders }); + } + + try { + const body: CreatePaymentRequest = await req.json(); + const { order_id, amount, description, method = 'qris' } = body; + + if (!order_id || !amount) { + return new Response( + JSON.stringify({ success: false, error: "order_id and amount are required" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const PAYMENT_PROJECT_SLUG = Deno.env.get("PAKASIR_PROJECT_SLUG") || ""; + const PAYMENT_API_KEY = Deno.env.get("PAKASIR_API_KEY") || ""; + const PAYMENT_CALLBACK_URL = `${Deno.env.get("SUPABASE_URL")}/functions/v1/pakasir-webhook`; + + if (!PAYMENT_PROJECT_SLUG || !PAYMENT_API_KEY) { + console.error("[PAYMENT] Missing credentials"); + return new Response( + JSON.stringify({ success: false, error: "Payment provider credentials not configured" }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log("[PAYMENT] Creating payment transaction:", { order_id, amount, method }); + + if (method === 'paypal') { + // Return PayPal payment URL + const paypalUrl = `https://app.pakasir.com/paypal/${PAYMENT_PROJECT_SLUG}/${amount}?order_id=${order_id}`; + + return new Response( + JSON.stringify({ + success: true, + data: { + payment_url: paypalUrl, + method: 'paypal', + order_id: order_id, + } + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + // Default: QRIS - Call provider API + const paymentResponse = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project: PAYMENT_PROJECT_SLUG, + order_id: order_id, + amount: amount, + api_key: PAYMENT_API_KEY, + description: description || `Order ${order_id}`, + callback_url: PAYMENT_CALLBACK_URL, + }), + }); + + if (!paymentResponse.ok) { + const errorText = await paymentResponse.text(); + console.error("[PAYMENT] Provider API error:", paymentResponse.status, errorText); + + // Fallback: return direct payment URL + const fallbackUrl = `https://app.pakasir.com/pay/${PAYMENT_PROJECT_SLUG}/${amount}?order_id=${order_id}`; + + return new Response( + JSON.stringify({ + success: true, + data: { + payment_url: fallbackUrl, + method: 'qris', + fallback: true, + order_id: order_id, + } + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const result = await paymentResponse.json(); + console.log("[PAYMENT] Payment created:", result); + + // Return QRIS data for display in app + return new Response( + JSON.stringify({ + success: true, + data: { + method: 'qris', + qr_string: result.qr_string || result.qr || null, + expired_at: result.expired_at || null, + // Fallback URL if QR code is not available + payment_url: `https://app.pakasir.com/pay/${PAYMENT_PROJECT_SLUG}/${amount}?order_id=${order_id}`, + order_id: order_id, + } + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + + } catch (error: any) { + console.error("[PAYMENT] Unexpected error:", error); + return new Response( + JSON.stringify({ + success: false, + error: error.message || "Internal server error" + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +});