Apply patch [skip ci]
This commit is contained in:
parent
a703c73aac
commit
084e310d26
123
routes/api/payments/webhook.ts
Normal file
123
routes/api/payments/webhook.ts
Normal file
@ -0,0 +1,123 @@
|
||||
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" },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user