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