import { define } from "../../../utils.ts"; import { createPaymentTransaction, getTransactionByStripeId, } from "../../../services/paymentService.ts"; async function hmacSHA256Hex(key: string, message: string): Promise { const enc = new TextEncoder(); const cryptoKey = await crypto.subtle.importKey( "raw", enc.encode(key), { name: "HMAC", hash: { name: "SHA-256" } }, false, ["sign"], ); const sig = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(message)); const bytes = new Uint8Array(sig); return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""); } function parseStripeSignature(header: string | null) { if (!header) return null; const parts = header.split(",").map((p) => p.trim()); const out: Record = {}; for (const part of parts) { const [k, v] = part.split("="); if (k && v) out[k] = v; } if (!out["t"] || !out["v1"]) return null; return { timestamp: out["t"], signature: out["v1"] }; } export const handler = define.handlers({ async POST(ctx) { try { const raw = await ctx.req.text(); const sandbox = Deno.env.get("DISABLE_EXTERNAL_APIS") === "true" || Deno.env.get("SANDBOX_MODE") === "true"; if (sandbox) { // Accept simulated event: { type, data: { id, amount, lease_id, payment_method } } const { type, data } = JSON.parse(raw || "{}"); if (type === "payment_intent.succeeded" && data?.id) { const existing = await getTransactionByStripeId(data.id); if (!existing) { const amountNumber = typeof data.amount === "number" ? data.amount : Number(data.amount); const amount = Number.isFinite(amountNumber) ? amountNumber : 0; await createPaymentTransaction( data.lease_id, amount, Date.now(), data.payment_method || "ach", "completed", data.id, ); } } return new Response(JSON.stringify({ received: true }), { status: 200, headers: { "Content-Type": "application/json" }, }); } // Production Stripe verification const secret = Deno.env.get("STRIPE_WEBHOOK_SECRET"); const sigHeader = ctx.req.headers.get("stripe-signature"); if (!secret || !sigHeader) { return new Response(JSON.stringify({ error: "Missing signature" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const parsed = parseStripeSignature(sigHeader); if (!parsed) { return new Response(JSON.stringify({ error: "Bad signature header" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const signedPayload = `${parsed.timestamp}.${raw}`; const expected = await hmacSHA256Hex(secret, signedPayload); if (expected !== parsed.signature) { return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const event = JSON.parse(raw || "{}"); if (event?.type === "payment_intent.succeeded") { const obj = event.data?.object ?? {}; const stripeId = obj.id as string; const amountCents = obj.amount_received ?? obj.amount ?? 0; const amount = Math.round(Number(amountCents)) / 100; const leaseId = obj.metadata?.lease_id ?? ""; const method = obj.payment_method_types?.[0] ?? "card"; const existing = await getTransactionByStripeId(stripeId); if (!existing) { await createPaymentTransaction( leaseId, amount, Date.now(), method, "completed", stripeId, ); } } return new Response(JSON.stringify({ received: true }), { status: 200, headers: { "Content-Type": "application/json" }, }); } catch (err) { return new Response(JSON.stringify({ error: String(err) }), { status: 500, headers: { "Content-Type": "application/json" }, }); } }, });