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>
152 lines
5.8 KiB
TypeScript
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" },
|
|
});
|
|
}
|
|
}); |