mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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" },
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user