Files
pi-skill/skills/scripts/templates/src/lib/stripe.ts
2026-05-25 16:41:08 +07:00

295 lines
6.8 KiB
TypeScript

/**
* 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<string, string>;
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<PaymentIntentResult> {
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<void>;
onPaymentIntentFailed?: (intent: Stripe.PaymentIntent) => Promise<void>;
onPaymentIntentProcessing?: (intent: Stripe.PaymentIntent) => Promise<void>;
}
) {
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<string, string>;
}) {
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<string, string>;
}
) {
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<string, string>;
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');
}