mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
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 the calling user
|
|
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 body = await req.json();
|
|
const { association_id, owner_id, unit_id, payment_method_type } = body;
|
|
|
|
if (!association_id) {
|
|
return new Response(
|
|
JSON.stringify({ error: "association_id is required" }),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Get Stripe mapping for this association
|
|
const { data: mapping, error: mappingError } = await supabase
|
|
.from("stripe_account_mappings")
|
|
.select("*")
|
|
.eq("association_id", association_id)
|
|
.eq("is_active", true)
|
|
.maybeSingle();
|
|
|
|
if (mappingError || !mapping?.stripe_secret_key) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "No active Stripe configuration found for this association.",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
const stripeSecretKey = mapping.stripe_secret_key;
|
|
|
|
// Get or create Stripe customer
|
|
// Check if there's already an enrollment with a customer ID for this user+association
|
|
const { data: existingEnrollment } = await supabase
|
|
.from("autopay_enrollments")
|
|
.select("stripe_customer_id")
|
|
.eq("association_id", association_id)
|
|
.eq("enrolled_by", user.id)
|
|
.not("stripe_customer_id", "is", null)
|
|
.limit(1)
|
|
.maybeSingle();
|
|
|
|
let stripeCustomerId = existingEnrollment?.stripe_customer_id;
|
|
|
|
if (!stripeCustomerId) {
|
|
// Create a new Stripe customer
|
|
const customerParams = new URLSearchParams();
|
|
customerParams.append("email", user.email || "");
|
|
customerParams.append("metadata[user_id]", user.id);
|
|
customerParams.append("metadata[association_id]", association_id);
|
|
|
|
const customerRes = await fetch(
|
|
"https://api.stripe.com/v1/customers",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${stripeSecretKey}`,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: customerParams.toString(),
|
|
}
|
|
);
|
|
|
|
const customerData = await customerRes.json();
|
|
if (!customerRes.ok) {
|
|
console.error("Stripe customer error:", customerData);
|
|
return new Response(
|
|
JSON.stringify({
|
|
error:
|
|
customerData.error?.message || "Failed to create Stripe customer",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
stripeCustomerId = customerData.id;
|
|
}
|
|
|
|
// Create a SetupIntent to save a payment method
|
|
const setupParams = new URLSearchParams();
|
|
setupParams.append("customer", stripeCustomerId!);
|
|
setupParams.append("usage", "off_session");
|
|
|
|
const isAch = payment_method_type === "us_bank_account";
|
|
if (isAch) {
|
|
setupParams.append("payment_method_types[]", "us_bank_account");
|
|
setupParams.append(
|
|
"payment_method_options[us_bank_account][financial_connections][permissions][]",
|
|
"payment_method"
|
|
);
|
|
} else {
|
|
setupParams.append("automatic_payment_methods[enabled]", "true");
|
|
}
|
|
|
|
setupParams.append("metadata[association_id]", association_id);
|
|
setupParams.append("metadata[user_id]", user.id);
|
|
if (owner_id) setupParams.append("metadata[owner_id]", owner_id);
|
|
if (unit_id) setupParams.append("metadata[unit_id]", unit_id);
|
|
|
|
const setupRes = await fetch(
|
|
"https://api.stripe.com/v1/setup_intents",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${stripeSecretKey}`,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: setupParams.toString(),
|
|
}
|
|
);
|
|
|
|
const setupData = await setupRes.json();
|
|
if (!setupRes.ok) {
|
|
console.error("Stripe SetupIntent error:", setupData);
|
|
return new Response(
|
|
JSON.stringify({
|
|
error:
|
|
setupData.error?.message || "Failed to create SetupIntent",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
clientSecret: setupData.client_secret,
|
|
setupIntentId: setupData.id,
|
|
customerId: stripeCustomerId,
|
|
publishableKey: mapping.stripe_publishable_key,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
} catch (err: unknown) {
|
|
console.error("Error in setup-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" },
|
|
});
|
|
}
|
|
});
|