import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import Stripe from 'https://esm.sh/stripe@14?target=deno'; const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, { apiVersion: '2023-10-16' }); const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!; serve(async (req) => { const body = await req.text(); const sig = req.headers.get('stripe-signature'); let event: Stripe.Event; try { event = await stripe.webhooks.constructEventAsync(body, sig!, webhookSecret); } catch (err) { console.error('Webhook signature failed:', err.message); return new Response(`Webhook Error: ${err.message}`, { status: 400 }); } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, ); // Helper: retrieve fee from a payment intent and mark invoice paid async function markPaid(paymentIntentId: string, invoice_id: string) { let stripe_fee: number | null = null; try { const pi = await stripe.paymentIntents.retrieve(paymentIntentId, { expand: ['latest_charge.balance_transaction'], }); const charge = pi.latest_charge as Stripe.Charge | null; const balanceTx = charge?.balance_transaction as Stripe.BalanceTransaction | null; if (balanceTx?.fee != null) stripe_fee = balanceTx.fee / 100; } catch (err) { console.error('Failed to retrieve Stripe fee:', err.message); } const updateData: Record = { status: 'paid', paid_at: new Date().toISOString(), }; if (stripe_fee !== null) updateData.stripe_fee = stripe_fee; await supabase.from('invoices').update(updateData).eq('id', invoice_id); } // Card payments: session completes with payment_status = 'paid' immediately if (event.type === 'checkout.session.completed') { const session = event.data.object as Stripe.Checkout.Session; const invoice_id = session.metadata?.invoice_id; const paymentIntentId = session.payment_intent as string | null; // Only mark paid here for instant payment methods (card). // ACH sessions complete with payment_status 'unpaid' — let payment_intent.succeeded handle those. if (invoice_id && paymentIntentId && session.payment_status === 'paid') { await markPaid(paymentIntentId, invoice_id); } } // ACH / async payment methods: payment_intent.succeeded fires when money actually settles if (event.type === 'payment_intent.succeeded') { const pi = event.data.object as Stripe.PaymentIntent; const invoice_id = pi.metadata?.invoice_id; if (invoice_id) { await markPaid(pi.id, invoice_id); } } return new Response(JSON.stringify({ received: true }), { status: 200 }); });