Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
@@ -0,0 +1,86 @@
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",
};
serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
try {
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
const { booking_id, amount_cents } = await req.json();
if (!booking_id || !amount_cents || amount_cents <= 0) {
return new Response(JSON.stringify({ error: "booking_id and positive amount_cents required" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
const { data: booking, error: bErr } = await supabase
.from("amenity_bookings")
.select("*, amenities(name), associations(name)")
.eq("id", booking_id).single();
if (bErr || !booking) return new Response(JSON.stringify({ error: "Booking not found" }), { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } });
if (!booking.guest_email) return new Response(JSON.stringify({ error: "Booking has no contact email" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
// Find Stripe mapping
let { data: mapping } = await supabase.from("stripe_account_mappings").select("*").eq("association_id", booking.association_id).eq("is_active", true).maybeSingle();
if (!mapping) {
const { data: company } = await supabase.from("stripe_account_mappings").select("*").is("association_id", null).eq("is_active", true).maybeSingle();
mapping = company;
}
if (!mapping?.stripe_secret_key) return new Response(JSON.stringify({ error: "No Stripe gateway configured" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
// Gross-up to pass processing fees to payer
const totalCents = Math.round((amount_cents + 30) / (1 - 0.029));
const origin = req.headers.get("origin") || "https://avria.cloud";
const params = new URLSearchParams();
params.append("mode", "payment");
params.append("line_items[0][price_data][currency]", "usd");
params.append("line_items[0][price_data][unit_amount]", String(totalCents));
params.append("line_items[0][price_data][product_data][name]", `Booking: ${booking.title || booking.amenities?.name || "Amenity"}`);
params.append("line_items[0][quantity]", "1");
params.append("customer_email", booking.guest_email);
params.append("success_url", `${origin}/booking/${booking.id}?paid=1`);
params.append("cancel_url", `${origin}/booking/${booking.id}`);
params.append("metadata[booking_id]", booking.id);
params.append("metadata[type]", "amenity_booking_link");
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: params.toString(),
});
const session = await stripeResp.json();
if (!stripeResp.ok) return new Response(JSON.stringify({ error: session.error?.message || "Stripe error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
await supabase.from("amenity_bookings").update({
payment_status: "link_sent",
payment_amount_cents: amount_cents,
payment_link_url: session.url,
}).eq("id", booking.id);
// Send email with link via existing transactional template
try {
await supabase.functions.invoke("send-transactional-email", {
body: {
templateName: "amenity-booking-confirmation",
recipientEmail: booking.guest_email,
idempotencyKey: `booking-payment-link-${booking.id}-${Date.now()}`,
templateData: {
guestName: booking.guest_name,
amenityName: booking.amenities?.name || "Amenity",
associationName: booking.associations?.name || "",
bookingDate: booking.booking_date,
startTime: booking.start_time || "",
status: "payment_due",
confirmationLink: session.url,
},
},
});
} catch (e) { console.error("email send failed", e); }
return new Response(JSON.stringify({ success: true, checkout_url: session.url }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
} catch (e: any) {
return new Response(JSON.stringify({ error: e.message || "Internal error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
});