mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user