Files
2026-06-01 20:19:26 -04:00

236 lines
7.9 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.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);
}
}