Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
294
skills/scripts/templates/src/lib/stripe.ts
Normal file
294
skills/scripts/templates/src/lib/stripe.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
Reference in New Issue
Block a user