128 lines
3.2 KiB
TypeScript
128 lines
3.2 KiB
TypeScript
import Stripe from "stripe"
|
|
|
|
let stripeClient: Stripe | null = null
|
|
|
|
export function getStripe(): Stripe | null {
|
|
if (stripeClient) return stripeClient
|
|
|
|
const key = process.env.STRIPE_SECRET_KEY
|
|
if (!key || key === "sk_test_REPLACE_ME") return null
|
|
|
|
stripeClient = new Stripe(key, {
|
|
apiVersion: "2025-01-27.acacia" as Stripe.LatestApiVersion,
|
|
typescript: true,
|
|
})
|
|
return stripeClient
|
|
}
|
|
|
|
/**
|
|
* Create a Stripe Checkout Session for a card payment.
|
|
* Returns the checkout URL to redirect the donor to.
|
|
*/
|
|
export async function createCheckoutSession(opts: {
|
|
amountPence: number
|
|
currency: string
|
|
pledgeId: string
|
|
reference: string
|
|
eventName: string
|
|
organizationName: string
|
|
donorEmail?: string
|
|
successUrl: string
|
|
cancelUrl: string
|
|
}): Promise<{ sessionId: string; checkoutUrl: string } | null> {
|
|
const stripe = getStripe()
|
|
if (!stripe) return null
|
|
|
|
try {
|
|
const session = await stripe.checkout.sessions.create({
|
|
mode: "payment",
|
|
payment_method_types: ["card"],
|
|
line_items: [
|
|
{
|
|
price_data: {
|
|
currency: opts.currency.toLowerCase(),
|
|
unit_amount: opts.amountPence,
|
|
product_data: {
|
|
name: `Donation — ${opts.eventName}`,
|
|
description: `Pledge ref: ${opts.reference} to ${opts.organizationName}`,
|
|
},
|
|
},
|
|
quantity: 1,
|
|
},
|
|
],
|
|
customer_email: opts.donorEmail || undefined,
|
|
metadata: {
|
|
pledge_id: opts.pledgeId,
|
|
reference: opts.reference,
|
|
},
|
|
success_url: opts.successUrl,
|
|
cancel_url: opts.cancelUrl,
|
|
})
|
|
|
|
return {
|
|
sessionId: session.id,
|
|
checkoutUrl: session.url!,
|
|
}
|
|
} catch (error) {
|
|
console.error("Stripe checkout session error:", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a Stripe Payment Intent for embedded payment (Stripe Elements).
|
|
* Returns client secret for frontend confirmation.
|
|
*/
|
|
export async function createPaymentIntent(opts: {
|
|
amountPence: number
|
|
currency: string
|
|
pledgeId: string
|
|
reference: string
|
|
donorEmail?: string
|
|
}): Promise<{ clientSecret: string; paymentIntentId: string } | null> {
|
|
const stripe = getStripe()
|
|
if (!stripe) return null
|
|
|
|
try {
|
|
const pi = await stripe.paymentIntents.create({
|
|
amount: opts.amountPence,
|
|
currency: opts.currency.toLowerCase(),
|
|
metadata: {
|
|
pledge_id: opts.pledgeId,
|
|
reference: opts.reference,
|
|
},
|
|
receipt_email: opts.donorEmail || undefined,
|
|
automatic_payment_methods: {
|
|
enabled: true,
|
|
},
|
|
})
|
|
|
|
return {
|
|
clientSecret: pi.client_secret!,
|
|
paymentIntentId: pi.id,
|
|
}
|
|
} catch (error) {
|
|
console.error("Stripe payment intent error:", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a Stripe webhook signature.
|
|
*/
|
|
export function constructWebhookEvent(
|
|
body: string | Buffer,
|
|
signature: string
|
|
): Stripe.Event | null {
|
|
const stripe = getStripe()
|
|
const secret = process.env.STRIPE_WEBHOOK_SECRET
|
|
if (!stripe || !secret || secret === "whsec_REPLACE_ME") return null
|
|
|
|
try {
|
|
return stripe.webhooks.constructEvent(body, signature, secret)
|
|
} catch (error) {
|
|
console.error("Stripe webhook signature verification failed:", error)
|
|
return null
|
|
}
|
|
}
|