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", }; Deno.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); // Verify admin/manager const authHeader = req.headers.get("Authorization"); if (!authHeader) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const token = authHeader.replace("Bearer ", ""); const { data: { user }, error: authError, } = await supabase.auth.getUser(token); if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const { data: roles } = await supabase .from("user_roles") .select("role") .eq("user_id", user.id); const isAdmin = roles?.some((r: any) => ["admin", "manager"].includes(r.role) ); if (!isAdmin) { return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const body = await req.json(); const { association_id, amount_cents, description, enrollment_ids } = body; if (!association_id || !amount_cents || amount_cents <= 0) { return new Response( JSON.stringify({ error: "association_id and valid amount_cents are required", }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Get Stripe mapping const { data: mapping } = await supabase .from("stripe_account_mappings") .select("*") .eq("association_id", association_id) .eq("is_active", true) .maybeSingle(); if (!mapping?.stripe_secret_key) { return new Response( JSON.stringify({ error: "No Stripe configuration for this association" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Get active enrollments let enrollmentQuery = supabase .from("autopay_enrollments") .select("*") .eq("association_id", association_id) .eq("is_active", true); if (enrollment_ids?.length) { enrollmentQuery = enrollmentQuery.in("id", enrollment_ids); } const { data: enrollments, error: enrollError } = await enrollmentQuery; if (enrollError) throw enrollError; if (!enrollments || enrollments.length === 0) { return new Response( JSON.stringify({ error: "No active autopay enrollments found", results: [] }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } const stripeSecretKey = mapping.stripe_secret_key; const results: any[] = []; for (const enrollment of enrollments) { try { // Calculate fees if applicable let feeCents = 0; const isAch = enrollment.payment_method_type === "us_bank_account"; if (mapping.pass_processing_fee) { if (isAch) { feeCents = Math.min(Math.ceil(amount_cents * 0.008), 500); } else { const feePercent = Number(mapping.processing_fee_percent) || 0.029; const feeFixed = Number(mapping.processing_fee_fixed_cents) || 30; const grossed = Math.ceil( (amount_cents + feeFixed) / (1 - feePercent) ); feeCents = grossed - amount_cents; } } const totalCents = amount_cents + feeCents; // Create PaymentIntent with saved payment method const params = new URLSearchParams(); params.append("amount", String(totalCents)); params.append("currency", "usd"); params.append("customer", enrollment.stripe_customer_id); params.append("payment_method", enrollment.stripe_payment_method_id); params.append("off_session", "true"); params.append("confirm", "true"); params.append( "description", description || "HOA Autopay Assessment" ); params.append("metadata[association_id]", association_id); params.append("metadata[enrollment_id]", enrollment.id); params.append("metadata[autopay]", "true"); if (enrollment.owner_id) params.append("metadata[owner_id]", enrollment.owner_id); if (enrollment.unit_id) params.append("metadata[unit_id]", enrollment.unit_id); if (isAch) { params.append("payment_method_types[]", "us_bank_account"); } const stripeRes = await fetch( "https://api.stripe.com/v1/payment_intents", { method: "POST", headers: { Authorization: `Bearer ${stripeSecretKey}`, "Content-Type": "application/x-www-form-urlencoded", }, body: params.toString(), } ); const stripeData = await stripeRes.json(); // Record payment await supabase.from("stripe_payments").insert({ association_id, owner_id: enrollment.owner_id || null, unit_id: enrollment.unit_id || null, stripe_payment_intent_id: stripeData.id, amount_cents, fee_cents: feeCents, total_cents: totalCents, payment_method_type: isAch ? "us_bank_account" : "card", status: stripeRes.ok ? "succeeded" : "failed", description: description || "HOA Autopay Assessment", }); results.push({ enrollment_id: enrollment.id, owner_id: enrollment.owner_id, success: stripeRes.ok, payment_intent_id: stripeData.id, amount_cents: totalCents, error: stripeRes.ok ? null : stripeData.error?.message, }); } catch (chargeErr: any) { results.push({ enrollment_id: enrollment.id, owner_id: enrollment.owner_id, success: false, error: chargeErr.message, }); } } const succeeded = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; return new Response( JSON.stringify({ results, summary: { total: results.length, succeeded, failed } }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } catch (err: unknown) { console.error("Error in process-autopay:", err); const message = err instanceof Error ? err.message : "Internal server error"; return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } });