Files
acmcc/supabase/functions/record-stripe-payment/index.ts
T
2026-06-01 20:19:26 -04:00

152 lines
5.8 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 supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
const authHeader = req.headers.get("Authorization");
if (!authHeader) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace("Bearer ", ""),
);
if (authError || !user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const { payment_intent_id } = await req.json();
if (!payment_intent_id) {
return new Response(JSON.stringify({ error: "payment_intent_id required" }), {
status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Load the stripe_payments row created when intent was made
const { data: payment, error: pErr } = await supabase
.from("stripe_payments")
.select("*")
.eq("stripe_payment_intent_id", payment_intent_id)
.maybeSingle();
if (pErr || !payment) {
return new Response(JSON.stringify({ error: "Payment record not found" }), {
status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Get Stripe secret key for the association
const { data: mapping } = await supabase
.from("stripe_account_mappings")
.select("stripe_secret_key, stripe_account_id")
.eq("association_id", payment.association_id)
.eq("is_active", true)
.maybeSingle();
if (!mapping?.stripe_secret_key) {
return new Response(JSON.stringify({ error: "Stripe not configured for association" }), {
status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Verify with Stripe that the PaymentIntent succeeded (or is processing for ACH)
const piRes = await fetch(`https://api.stripe.com/v1/payment_intents/${payment_intent_id}`, {
headers: {
Authorization: `Bearer ${mapping.stripe_secret_key}`,
...(mapping.stripe_account_id?.startsWith("acct_") ? { "Stripe-Account": mapping.stripe_account_id } : {}),
},
});
const pi = await piRes.json();
if (!piRes.ok) {
console.error("Stripe lookup error:", pi);
return new Response(JSON.stringify({ error: pi.error?.message || "Stripe error" }), {
status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const status = pi.status as string;
const recordable = status === "succeeded" || status === "processing";
if (!recordable) {
await supabase.from("stripe_payments")
.update({ status, updated_at: new Date().toISOString() })
.eq("id", payment.id);
return new Response(JSON.stringify({ ok: false, status }), {
status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Update stripe_payments status
await supabase.from("stripe_payments")
.update({ status, updated_at: new Date().toISOString() })
.eq("id", payment.id);
// Idempotency: skip if a ledger entry already exists for this PI
const { data: existing } = await supabase
.from("owner_ledger_entries")
.select("id")
.eq("reference_type", "stripe_payment")
.eq("reference_id", payment_intent_id)
.maybeSingle();
if (existing) {
return new Response(JSON.stringify({ ok: true, already_recorded: true }), {
status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (!payment.owner_id) {
return new Response(JSON.stringify({ ok: true, skipped: "no owner" }), {
status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const netAmount = Number(payment.amount_cents) / 100;
const today = new Date().toISOString().split("T")[0];
const methodLabel = payment.payment_method_type === "us_bank_account" ? "ACH" : "Card";
const { error: ledgerErr } = await supabase.from("owner_ledger_entries").insert({
owner_id: payment.owner_id,
association_id: payment.association_id,
unit_id: payment.unit_id,
date: today,
transaction_type: "payment",
description: `Online Payment (${methodLabel}) — ${payment.description || "Assessment"}`,
debit: 0,
credit: netAmount,
reference_type: "stripe_payment",
reference_id: payment_intent_id,
});
if (ledgerErr) {
console.error("Ledger insert error:", ledgerErr);
return new Response(JSON.stringify({ error: ledgerErr.message }), {
status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ ok: true, recorded: true, status }), {
status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (err) {
console.error("record-stripe-payment error:", err);
return new Response(JSON.stringify({ error: (err as Error).message }), {
status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});