/** * Stripe Payment Integration * * Supports: * - Payment Intents (Cards, etc.) * - Webhook handling * - Refunds */ import Stripe from 'stripe'; // Initialize Stripe client const stripeSecretKey = import.meta.env.STRIPE_SECRET_KEY; export const stripe = new Stripe(stripeSecretKey || 'sk_test_placeholder', { apiVersion: '2025-01-27.acacia' as any, }); // ============================================ // Types // ============================================ export interface CreatePaymentIntentParams { amount: number; // in satang (THB = 100 satang) currency?: string; metadata?: Record; customerId?: string; description?: string; } export interface PaymentIntentResult { clientSecret: string; paymentIntentId: string; } // ============================================ // Payment Intents // ============================================ /** * Create a payment intent for checkout * Amount should be in satang (baht * 100) */ export async function createPaymentIntent( params: CreatePaymentIntentParams ): Promise { const { amount, currency = 'thb', metadata = {}, customerId, description, } = params; const paymentIntent = await stripe.paymentIntents.create({ amount, currency, automatic_payment_methods: { enabled: true }, metadata, ...(customerId && { customer: customerId }), ...(description && { description }), }); if (!paymentIntent.client_secret) { throw new Error('Failed to create payment intent'); } return { clientSecret: paymentIntent.client_secret, paymentIntentId: paymentIntent.id, }; } /** * Retrieve a payment intent */ export async function getPaymentIntent(paymentIntentId: string) { return stripe.paymentIntents.retrieve(paymentIntentId); } /** * Confirm a payment intent */ export async function confirmPaymentIntent( paymentIntentId: string, paymentMethodId: string ) { return stripe.paymentIntents.confirm(paymentIntentId, { payment_method: paymentMethodId, }); } /** * Cancel a payment intent */ export async function cancelPaymentIntent(paymentIntentId: string) { return stripe.paymentIntents.cancel(paymentIntentId); } // ============================================ // Webhooks // ============================================ /** * Construct and verify webhook event */ export async function constructWebhookEvent( payload: string | Buffer, signature: string ) { const webhookSecret = import.meta.env.STRIPE_WEBHOOK_SECRET; if (!webhookSecret) { throw new Error('STRIPE_WEBHOOK_SECRET not configured'); } return stripe.webhooks.constructEvent(payload, signature, webhookSecret); } /** * Parse webhook event safely */ export async function handleWebhook( payload: string | Buffer, signature: string, handlers: { onPaymentIntentSucceeded?: (intent: Stripe.PaymentIntent) => Promise; onPaymentIntentFailed?: (intent: Stripe.PaymentIntent) => Promise; onPaymentIntentProcessing?: (intent: Stripe.PaymentIntent) => Promise; } ) { const event = await constructWebhookEvent(payload, signature); switch (event.type) { case 'payment_intent.succeeded': if (handlers.onPaymentIntentSucceeded) { await handlers.onPaymentIntentSucceeded(event.data.object as Stripe.PaymentIntent); } break; case 'payment_intent.payment_failed': if (handlers.onPaymentIntentFailed) { await handlers.onPaymentIntentFailed(event.data.object as Stripe.PaymentIntent); } break; case 'payment_intent.processing': if (handlers.onPaymentIntentProcessing) { await handlers.onPaymentIntentProcessing(event.data.object as Stripe.PaymentIntent); } break; default: console.log(`Unhandled webhook event type: ${event.type}`); } return event; } // ============================================ // Customers // ============================================ /** * Create a Stripe customer */ export async function createCustomer(params: { email: string; name?: string; metadata?: Record; }) { return stripe.customers.create({ email: params.email, name: params.name, metadata: params.metadata, }); } /** * Get customer by ID */ export async function getCustomer(customerId: string) { return stripe.customers.retrieve(customerId); } /** * Update customer */ export async function updateCustomer( customerId: string, params: { email?: string; name?: string; metadata?: Record; } ) { return stripe.customers.update(customerId, params); } // ============================================ // Refunds // ============================================ /** * Create a refund */ export async function createRefund(params: { paymentIntentId: string; amount?: number; // partial refund amount in satang reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'; }) { return stripe.refunds.create({ payment_intent: params.paymentIntentId, amount: params.amount, reason: params.reason, }); } /** * Get refund status */ export async function getRefund(refundId: string) { return stripe.refunds.retrieve(refundId); } // ============================================ // Checkout Sessions // ============================================ /** * Create a checkout session for hosted checkout */ export async function createCheckoutSession(params: { lineItems: Array<{ price_data?: { currency: string; product_data: { name: string; description?: string; images?: string[] }; unit_amount: number; }; price?: string; quantity: number; }>; successUrl: string; cancelUrl: string; customerEmail?: string; metadata?: Record; mode?: 'payment' | 'subscription'; }) { return stripe.checkout.sessions.create({ mode: params.mode || 'payment', line_items: params.lineItems, success_url: params.successUrl, cancel_url: params.cancelUrl, customer_email: params.customerEmail, metadata: params.metadata, }); } // ============================================ // Helpers // ============================================ /** * Convert baht to satang (THB * 100) */ export function bahtToSatang(amount: number): number { return Math.round(amount * 100); } /** * Convert satang to baht */ export function satangToBaht(amount: number): number { return amount / 100; } /** * Format amount for Stripe (in smallest currency unit) */ export function formatAmount(amount: number, currency = 'thb'): number { // For THB, no decimal places if (currency.toLowerCase() === 'thb') { return Math.round(amount); } // For others, convert to cents return Math.round(amount * 100); } /** * Check if Stripe is configured */ export function isStripeConfigured(): boolean { return !!stripeSecretKey && !stripeSecretKey.includes('placeholder'); }