import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.3"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", }; serve(async (req) => { 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 = await req.json(); const { association_id, amenity_id, pin_id, pin_label, guest_name, guest_email, guest_phone, booking_date, signature_data, amount_cents, success_url, cancel_url, form_data, booking_type, title, start_time, end_time, notes, } = body; if (!association_id || !amenity_id || !guest_name || !booking_date) { return new Response( JSON.stringify({ error: "Missing required fields" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Save the reservation as pending const { data: booking, error: bookingError } = await supabase .from("amenity_bookings") .insert({ amenity_id, association_id, guest_name, guest_email: guest_email || null, guest_phone: guest_phone || null, booking_date, booking_type: booking_type || "rental", title: title || `Reservation: ${pin_label || "Amenity"}`, start_time: start_time || null, end_time: end_time || null, form_data: form_data || {}, notes: notes || JSON.stringify({ pin_id, pin_label, signature: signature_data || null, form_data: form_data || null, }), status: amount_cents > 0 ? "pending_payment" : "confirmed", }) .select() .single(); if (bookingError) { return new Response( JSON.stringify({ error: bookingError.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Create form inbox entry await supabase.from("form_inbox").insert({ source_type: "reservation", source_id: booking.id, association_id, title: `Reservation: ${pin_label || "Amenity Spot"}`, submitter_name: guest_name, submitter_email: guest_email || null, summary: `${pin_label || "Spot"} | Date: ${booking_date} | Fee: ${amount_cents > 0 ? "$" + (amount_cents / 100).toFixed(2) : "Free"}`, status: "new", }); // Send in-app notifications to admin/manager users try { const { data: adminRoles } = await supabase .from("user_roles") .select("user_id") .in("role", ["admin", "manager"]); if (adminRoles && adminRoles.length > 0) { for (const role of adminRoles) { await supabase.rpc("insert_notification", { p_user_id: role.user_id, p_type: "reservation", p_title: `New Reservation: ${pin_label || "Amenity Spot"}`, p_message: `${guest_name} submitted a reservation for ${pin_label || "a spot"} on ${booking_date}.`, p_related_item_id: booking.id, p_related_item_type: "amenity_booking", }); } } } catch (notifErr) { console.error("Failed to send admin notifications:", notifErr); } // If no fee — mark pin as rented immediately if (!amount_cents || amount_cents <= 0) { await markPinAsRented(supabase, amenity_id, pin_id); return new Response( JSON.stringify({ success: true, booking_id: booking.id }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Look up Stripe mapping for this association const { data: stripeMapping } = await supabase .from("stripe_account_mappings") .select("*") .eq("association_id", association_id) .eq("is_active", true) .maybeSingle(); let mapping = stripeMapping; if (!mapping) { const { data: companyMapping } = await supabase .from("stripe_account_mappings") .select("*") .is("association_id", null) .eq("is_active", true) .maybeSingle(); mapping = companyMapping; } if (!mapping?.stripe_secret_key) { return new Response( JSON.stringify({ success: true, booking_id: booking.id, payment_note: "No payment gateway configured. Reservation saved as pending.", }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Calculate fee — always pass processing fees to the payer for reservations let totalCents = Math.round((amount_cents + 30) / (1 - 0.029)); // Create Stripe Checkout Session const stripeParams = new URLSearchParams(); stripeParams.append("mode", "payment"); stripeParams.append("line_items[0][price_data][currency]", "usd"); stripeParams.append("line_items[0][price_data][unit_amount]", String(totalCents)); stripeParams.append("line_items[0][price_data][product_data][name]", `Reservation: ${pin_label || "Amenity"}`); stripeParams.append("line_items[0][quantity]", "1"); stripeParams.append("customer_email", guest_email || ""); stripeParams.append("success_url", `${success_url}?session_id={CHECKOUT_SESSION_ID}&booking_id=${booking.id}`); stripeParams.append("cancel_url", cancel_url || success_url); stripeParams.append("metadata[booking_id]", booking.id); stripeParams.append("metadata[association_id]", association_id); stripeParams.append("metadata[amenity_id]", amenity_id); stripeParams.append("metadata[pin_id]", pin_id || ""); const stripeResp = await fetch("https://api.stripe.com/v1/checkout/sessions", { method: "POST", headers: { Authorization: `Bearer ${mapping.stripe_secret_key}`, "Content-Type": "application/x-www-form-urlencoded", }, body: stripeParams.toString(), }); const session = await stripeResp.json(); if (!stripeResp.ok) { console.error("Stripe error:", session); return new Response( JSON.stringify({ error: session.error?.message || "Stripe checkout failed" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } return new Response( JSON.stringify({ success: true, booking_id: booking.id, checkout_url: session.url, }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (err) { console.error("Error in create-reservation-checkout:", err); return new Response( JSON.stringify({ error: err.message || "Internal server error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } }); // Helper: update the pin status to "rented" in the amenity's map_config async function markPinAsRented(supabase: any, amenityId: string, pinId: string) { try { const { data: amenity } = await supabase .from("amenities") .select("map_config") .eq("id", amenityId) .single(); if (!amenity?.map_config?.pins) return; const updatedPins = amenity.map_config.pins.map((pin: any) => pin.id === pinId ? { ...pin, status: "rented" } : pin ); await supabase .from("amenities") .update({ map_config: { ...amenity.map_config, pins: updatedPins }, updated_at: new Date().toISOString(), }) .eq("id", amenityId); } catch (err) { console.error("Failed to mark pin as rented:", err); } }