mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
6.0 KiB
TypeScript
172 lines
6.0 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);
|
|
|
|
// 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 {
|
|
amount_cents,
|
|
association_id,
|
|
owner_id,
|
|
unit_id,
|
|
description,
|
|
payment_method_type,
|
|
} = body;
|
|
|
|
if (!amount_cents || amount_cents <= 0) {
|
|
return new Response(JSON.stringify({ error: "Invalid amount" }), {
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
if (!stripeSecretKey) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Stripe secret key is not configured for this association." }),
|
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
const stripeHeaders: Record<string, string> = {
|
|
Authorization: `Bearer ${stripeSecretKey}`,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
};
|
|
if (mapping.stripe_account_id?.startsWith("acct_")) {
|
|
stripeHeaders["Stripe-Account"] = mapping.stripe_account_id;
|
|
}
|
|
|
|
// Calculate fees if pass_processing_fee is on
|
|
let feeCents = 0;
|
|
const isAch = 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 via Stripe API
|
|
const stripeParams = new URLSearchParams();
|
|
stripeParams.append("amount", String(totalCents));
|
|
stripeParams.append("currency", "usd");
|
|
stripeParams.append("description", description || "HOA Assessment Payment");
|
|
stripeParams.append("metadata[association_id]", association_id);
|
|
stripeParams.append("metadata[user_id]", user.id);
|
|
if (owner_id) stripeParams.append("metadata[owner_id]", owner_id);
|
|
if (unit_id) stripeParams.append("metadata[unit_id]", unit_id);
|
|
stripeParams.append("metadata[net_amount_cents]", String(amount_cents));
|
|
stripeParams.append("metadata[fee_cents]", String(feeCents));
|
|
|
|
if (isAch) {
|
|
stripeParams.append("payment_method_types[]", "us_bank_account");
|
|
} else {
|
|
stripeParams.append("automatic_payment_methods[enabled]", "true");
|
|
}
|
|
|
|
const stripeRes = await fetch("https://api.stripe.com/v1/payment_intents", {
|
|
method: "POST",
|
|
headers: stripeHeaders,
|
|
body: stripeParams.toString(),
|
|
});
|
|
|
|
const stripeData = await stripeRes.json();
|
|
if (!stripeRes.ok) {
|
|
console.error("Stripe error:", stripeData);
|
|
return new Response(
|
|
JSON.stringify({ error: stripeData.error?.message || "Stripe API error" }),
|
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Record the payment in our database
|
|
await supabase.from("stripe_payments").insert({
|
|
association_id,
|
|
owner_id: owner_id || null,
|
|
unit_id: 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: "pending",
|
|
description: description || "HOA Assessment Payment",
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
clientSecret: stripeData.client_secret,
|
|
paymentIntentId: stripeData.id,
|
|
totalCents,
|
|
feeCents,
|
|
}),
|
|
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
} catch (err) {
|
|
console.error("Error in create-payment-intent:", err);
|
|
return new Response(
|
|
JSON.stringify({ error: err.message || "Internal server error" }),
|
|
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
});
|