open-swe-dev-andrinoid[bot] 084e310d26 Apply patch [skip ci]
2026-01-21 19:19:08 +00:00

124 lines
4.2 KiB
TypeScript

import { define } from "../../../utils.ts";
import {
createPaymentTransaction,
getTransactionByStripeId,
} from "../../../services/paymentService.ts";
async function hmacSHA256Hex(key: string, message: string): Promise<string> {
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<string, string> = {};
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" },
});
}
},
});