payment-integration

doanchienthangdev's avatarfrom doanchienthangdev

Enterprise payment processing with Stripe, PayPal, and LemonSqueezy including subscriptions, webhooks, and PCI compliance

0stars🔀0forks📁View on GitHub🕐Updated Jan 8, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for enterprise-grade payment integration, supporting major providers like Stripe, PayPal, and LemonSqueezy. It streamlines the implementation of secure checkout flows, recurring subscription management, and automated webhook handling while ensuring strict PCI DSS compliance and best practices for modern web applications.

Use Cases

  • E-commerce Checkout Systems: Implementing secure one-time payment flows with automated tax calculation, shipping collection, and order management.
  • SaaS Subscription Billing: Building robust recurring billing systems that handle trial periods, plan upgrades/downgrades, prorations, and cancellations.
  • Automated Webhook Processing: Setting up reliable server-side listeners to synchronize payment statuses, handle failed invoices, and trigger customer lifecycle events in real-time.
  • Secure Refund and Dispute Management: Developing programmatic interfaces to process partial or full refunds and manage chargeback evidence while maintaining a clear audit trail.
  • Multi-Platform Payment Routing: Integrating multiple payment gateways (Stripe, PayPal, LemonSqueezy) to offer customers diverse payment options and global coverage.
namepayment-integration
descriptionEnterprise payment processing with Stripe, PayPal, and LemonSqueezy including subscriptions, webhooks, and PCI compliance
categoryintegrations

Payment Integration

Enterprise-grade payment processing with Stripe, PayPal, and LemonSqueezy. This skill covers checkout flows, subscription management, webhook handling, and PCI compliance patterns.

Purpose

Implement secure, reliable payment systems:

  • Process one-time and recurring payments
  • Handle subscription lifecycle management
  • Implement secure webhook processing
  • Manage refunds and disputes
  • Ensure PCI DSS compliance
  • Support multiple payment methods

Features

1. Stripe Integration

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
  typescript: true,
});

// Create checkout session
async function createCheckoutSession(
  items: CartItem[],
  customerId?: string,
  metadata?: Record<string, string>
): Promise<Stripe.Checkout.Session> {
  const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = items.map(item => ({
    price_data: {
      currency: 'usd',
      product_data: {
        name: item.name,
        description: item.description,
        images: item.images,
        metadata: { productId: item.id },
      },
      unit_amount: Math.round(item.price * 100), // Convert to cents
    },
    quantity: item.quantity,
  }));

  return stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: lineItems,
    customer: customerId,
    success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/checkout/cancel`,
    metadata,
    payment_intent_data: {
      metadata,
    },
    shipping_address_collection: {
      allowed_countries: ['US', 'CA', 'GB'],
    },
    automatic_tax: { enabled: true },
  });
}

// Create subscription
async function createSubscription(
  customerId: string,
  priceId: string,
  options?: {
    trialDays?: number;
    couponId?: string;
    metadata?: Record<string, string>;
  }
): Promise<Stripe.Subscription> {
  const params: Stripe.SubscriptionCreateParams = {
    customer: customerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete',
    payment_settings: {
      save_default_payment_method: 'on_subscription',
    },
    expand: ['latest_invoice.payment_intent'],
    metadata: options?.metadata,
  };

  if (options?.trialDays) {
    params.trial_period_days = options.trialDays;
  }

  if (options?.couponId) {
    params.coupon = options.couponId;
  }

  return stripe.subscriptions.create(params);
}

// Handle subscription changes
async function updateSubscription(
  subscriptionId: string,
  newPriceId: string,
  prorationBehavior: 'create_prorations' | 'none' | 'always_invoice' = 'create_prorations'
): Promise<Stripe.Subscription> {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: subscription.items.data[0].id,
      price: newPriceId,
    }],
    proration_behavior: prorationBehavior,
  });
}

// Cancel subscription
async function cancelSubscription(
  subscriptionId: string,
  cancelImmediately: boolean = false
): Promise<Stripe.Subscription> {
  if (cancelImmediately) {
    return stripe.subscriptions.cancel(subscriptionId);
  }

  return stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true,
  });
}

// Process refund
async function processRefund(
  paymentIntentId: string,
  amount?: number, // Partial refund in cents
  reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
): Promise<Stripe.Refund> {
  return stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount, // Omit for full refund
    reason,
  });
}

2. Webhook Handling

import { buffer } from 'micro';
import type { NextApiRequest, NextApiResponse } from 'next';

// Webhook handler with signature verification
async function handleStripeWebhook(
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
    return;
  }

  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'] as string;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      buf,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    res.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  // Handle events with idempotency
  const idempotencyKey = event.id;
  const processed = await isEventProcessed(idempotencyKey);

  if (processed) {
    res.status(200).json({ received: true, duplicate: true });
    return;
  }

  try {
    await processWebhookEvent(event);
    await markEventProcessed(idempotencyKey);
    res.status(200).json({ received: true });
  } catch (err) {
    console.error('Webhook processing error:', err);
    // Return 200 to prevent retries for business logic errors
    // Return 500 for transient errors that should be retried
    res.status(err.retryable ? 500 : 200).json({ error: err.message });
  }
}

// Event processor
async function processWebhookEvent(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleCheckoutComplete(session);
      break;
    }

    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      await syncSubscriptionStatus(subscription);
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCanceled(subscription);
      break;
    }

    case 'invoice.payment_succeeded': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentSuccess(invoice);
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailure(invoice);
      break;
    }

    case 'charge.dispute.created': {
      const dispute = event.data.object as Stripe.Dispute;
      await handleDispute(dispute);
      break;
    }

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
}

// Subscription sync
async function syncSubscriptionStatus(subscription: Stripe.Subscription): Promise<void> {
  await db.subscription.upsert({
    where: { stripeSubscriptionId: subscription.id },
    update: {
      status: subscription.status,
      currentPeriodStart: new Date(subscription.current_period_start * 1000),
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
      priceId: subscription.items.data[0].price.id,
    },
    create: {
      stripeSubscriptionId: subscription.id,
      stripeCustomerId: subscription.customer as string,
      status: subscription.status,
      currentPeriodStart: new Date(subscription.current_period_start * 1000),
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      priceId: subscription.items.data[0].price.id,
    },
  });
}

3. PayPal Integration

import { PayPalHttpClient, OrdersCreateRequest, OrdersCaptureRequest } from '@paypal/checkout-server-sdk';

// PayPal client setup
function getPayPalClient(): PayPalHttpClient {
  const environment = process.env.NODE_ENV === 'production'
    ? new paypal.core.LiveEnvironment(
        process.env.PAYPAL_CLIENT_ID!,
        process.env.PAYPAL_CLIENT_SECRET!
      )
    : new paypal.core.SandboxEnvironment(
        process.env.PAYPAL_CLIENT_ID!,
        process.env.PAYPAL_CLIENT_SECRET!
      );

  return new PayPalHttpClient(environment);
}

// Create PayPal order
async function createPayPalOrder(
  items: CartItem[],
  shippingCost: number = 0
): Promise<PayPalOrder> {
  const client = getPayPalClient();

  const itemTotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const total = itemTotal + shippingCost;

  const request = new OrdersCreateRequest();
  request.prefer('return=representation');
  request.requestBody({
    intent: 'CAPTURE',
    purchase_units: [{
      amount: {
        currency_code: 'USD',
        value: total.toFixed(2),
        breakdown: {
          item_total: { currency_code: 'USD', value: itemTotal.toFixed(2) },
          shipping: { currency_code: 'USD', value: shippingCost.toFixed(2) },
        },
      },
      items: items.map(item => ({
        name: item.name,
        unit_amount: { currency_code: 'USD', value: item.price.toFixed(2) },
        quantity: item.quantity.toString(),
        category: 'PHYSICAL_GOODS',
      })),
    }],
    application_context: {
      brand_name: process.env.APP_NAME,
      landing_page: 'BILLING',
      user_action: 'PAY_NOW',
      return_url: `${process.env.APP_URL}/checkout/paypal/success`,
      cancel_url: `${process.env.APP_URL}/checkout/paypal/cancel`,
    },
  });

  const response = await client.execute(request);
  return response.result;
}

// Capture PayPal payment
async function capturePayPalOrder(orderId: string): Promise<PayPalCapture> {
  const client = getPayPalClient();
  const request = new OrdersCaptureRequest(orderId);
  request.prefer('return=representation');

  const response = await client.execute(request);

  if (response.result.status === 'COMPLETED') {
    await handlePayPalPaymentComplete(response.result);
  }

  return response.result;
}

4. Subscription Management UI

// Subscription management component
interface SubscriptionManagerProps {
  subscription: UserSubscription;
  availablePlans: Plan[];
}

export function SubscriptionManager({ subscription, availablePlans }: SubscriptionManagerProps) {
  const [isLoading, setIsLoading] = useState(false);

  async function handleUpgrade(newPriceId: string) {
    setIsLoading(true);
    try {
      await updateSubscription(subscription.id, newPriceId);
      toast.success('Subscription updated successfully');
    } catch (error) {
      toast.error('Failed to update subscription');
    } finally {
      setIsLoading(false);
    }
  }

  async function handleCancel() {
    if (!confirm('Are you sure you want to cancel your subscription?')) return;

    setIsLoading(true);
    try {
      await cancelSubscription(subscription.id);
      toast.success('Subscription will be canceled at the end of the billing period');
    } catch (error) {
      toast.error('Failed to cancel subscription');
    } finally {
      setIsLoading(false);
    }
  }

  async function handleReactivate() {
    setIsLoading(true);
    try {
      await reactivateSubscription(subscription.id);
      toast.success('Subscription reactivated');
    } catch (error) {
      toast.error('Failed to reactivate subscription');
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div className="subscription-manager">
      <div className="current-plan">
        <h3>Current Plan: {subscription.plan.name}</h3>
        <p>Status: {subscription.status}</p>
        <p>Renews: {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}</p>
      </div>

      {subscription.cancelAtPeriodEnd && (
        <Alert variant="warning">
          Your subscription will end on {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}
          <Button onClick={handleReactivate} disabled={isLoading}>
            Reactivate
          </Button>
        </Alert>
      )}

      <div className="available-plans">
        <h4>Available Plans</h4>
        {availablePlans.map(plan => (
          <PlanCard
            key={plan.id}
            plan={plan}
            isCurrent={plan.priceId === subscription.priceId}
            onSelect={() => handleUpgrade(plan.priceId)}
            disabled={isLoading}
          />
        ))}
      </div>

      {!subscription.cancelAtPeriodEnd && (
        <Button variant="destructive" onClick={handleCancel} disabled={isLoading}>
          Cancel Subscription
        </Button>
      )}
    </div>
  );
}

5. LemonSqueezy Integration

import { lemonSqueezySetup, createCheckout, getSubscription } from '@lemonsqueezy/lemonsqueezy.js';

// Initialize LemonSqueezy
lemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY! });

// Create LemonSqueezy checkout
async function createLemonSqueezyCheckout(
  variantId: string,
  customerEmail: string,
  metadata?: Record<string, string>
): Promise<string> {
  const checkout = await createCheckout(
    process.env.LEMONSQUEEZY_STORE_ID!,
    variantId,
    {
      checkoutData: {
        email: customerEmail,
        custom: metadata,
      },
      checkoutOptions: {
        embed: false,
        logo: true,
        dark: false,
      },
      productOptions: {
        enabledVariants: [parseInt(variantId)],
        redirectUrl: `${process.env.APP_URL}/checkout/success`,
      },
    }
  );

  return checkout.data.attributes.url;
}

// LemonSqueezy webhook handler
async function handleLemonSqueezyWebhook(req: NextApiRequest): Promise<void> {
  const signature = req.headers['x-signature'] as string;
  const payload = JSON.stringify(req.body);

  // Verify signature
  const hmac = crypto.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);
  hmac.update(payload);
  const expectedSignature = hmac.digest('hex');

  if (signature !== expectedSignature) {
    throw new Error('Invalid webhook signature');
  }

  const { event_name, data } = req.body;

  switch (event_name) {
    case 'subscription_created':
      await handleLSSubscriptionCreated(data);
      break;
    case 'subscription_updated':
      await handleLSSubscriptionUpdated(data);
      break;
    case 'subscription_cancelled':
      await handleLSSubscriptionCancelled(data);
      break;
    case 'order_created':
      await handleLSOrderCreated(data);
      break;
  }
}

6. Payment Security

// PCI-compliant payment handling patterns

// Never log sensitive payment data
const SENSITIVE_FIELDS = ['card_number', 'cvv', 'cvc', 'exp_month', 'exp_year'];

function sanitizeForLogging(data: Record<string, any>): Record<string, any> {
  const sanitized = { ...data };

  for (const field of SENSITIVE_FIELDS) {
    if (field in sanitized) {
      sanitized[field] = '[REDACTED]';
    }
  }

  return sanitized;
}

// Secure API endpoint
async function createPaymentIntent(req: NextApiRequest, res: NextApiResponse) {
  // Validate request
  const { amount, currency, customerId } = req.body;

  if (!amount || amount < 50) { // Minimum $0.50
    return res.status(400).json({ error: 'Invalid amount' });
  }

  // Use idempotency key
  const idempotencyKey = req.headers['idempotency-key'] as string;

  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency key required' });
  }

  try {
    const paymentIntent = await stripe.paymentIntents.create(
      {
        amount,
        currency: currency || 'usd',
        customer: customerId,
        automatic_payment_methods: { enabled: true },
        metadata: {
          userId: req.user.id,
        },
      },
      { idempotencyKey }
    );

    // Only return necessary data
    res.json({
      clientSecret: paymentIntent.client_secret,
      paymentIntentId: paymentIntent.id,
    });
  } catch (error) {
    console.error('Payment error:', sanitizeForLogging(error));

    if (error.type === 'StripeCardError') {
      return res.status(400).json({ error: error.message });
    }

    res.status(500).json({ error: 'Payment processing failed' });
  }
}

// Webhook security middleware
function verifyWebhookSignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  try {
    stripe.webhooks.constructEvent(payload, signature, secret);
    return true;
  } catch {
    return false;
  }
}

Use Cases

1. E-commerce Checkout

// Complete e-commerce checkout flow
async function processCheckout(cart: Cart, user: User): Promise<CheckoutResult> {
  // Calculate totals
  const subtotal = calculateSubtotal(cart.items);
  const tax = await calculateTax(subtotal, user.address);
  const shipping = await calculateShipping(cart.items, user.address);
  const total = subtotal + tax + shipping;

  // Create Stripe checkout
  const session = await createCheckoutSession(cart.items, user.stripeCustomerId, {
    orderId: generateOrderId(),
    userId: user.id,
  });

  // Create pending order
  await db.order.create({
    data: {
      userId: user.id,
      status: 'pending',
      subtotal,
      tax,
      shipping,
      total,
      stripeSessionId: session.id,
      items: {
        create: cart.items.map(item => ({
          productId: item.productId,
          quantity: item.quantity,
          price: item.price,
        })),
      },
    },
  });

  return {
    checkoutUrl: session.url,
    sessionId: session.id,
  };
}

2. SaaS Subscription

// SaaS subscription management
async function handlePlanChange(
  userId: string,
  newPlanId: string
): Promise<PlanChangeResult> {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { subscription: true },
  });

  if (!user.subscription) {
    // New subscription
    const session = await createSubscriptionCheckout(user, newPlanId);
    return { action: 'redirect', url: session.url };
  }

  const currentPlan = await getPlan(user.subscription.priceId);
  const newPlan = await getPlan(newPlanId);

  if (newPlan.price > currentPlan.price) {
    // Upgrade - charge prorated amount
    await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'create_prorations');
    return { action: 'upgraded', newPlan };
  } else {
    // Downgrade - apply at period end
    await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'none');
    return { action: 'scheduled', effectiveDate: user.subscription.currentPeriodEnd };
  }
}

Best Practices

Do's

  • Use idempotency keys - Prevent duplicate charges
  • Verify webhook signatures - Always validate webhook authenticity
  • Handle all payment states - Success, failure, pending, disputed
  • Store payment references - Keep Stripe IDs for reconciliation
  • Test with sandbox - Use test mode during development
  • Monitor for fraud - Implement Stripe Radar or equivalent

Don'ts

  • Never log full card numbers
  • Never store CVV/CVC codes
  • Never handle raw card data (use Stripe.js/Elements)
  • Never skip signature verification
  • Never trust client-side amounts
  • Never expose secret keys in frontend

Security Checklist

## Payment Security Checklist

### PCI Compliance
- [ ] No raw card data on server
- [ ] Using Stripe.js/Elements for card collection
- [ ] Webhook signature verification enabled
- [ ] HTTPS only for all payment endpoints

### Data Handling
- [ ] Sensitive fields excluded from logs
- [ ] Customer IDs used instead of card details
- [ ] Idempotency keys for all mutations
- [ ] Payment references stored securely

### Fraud Prevention
- [ ] Stripe Radar enabled
- [ ] Address verification (AVS)
- [ ] 3D Secure enabled
- [ ] Velocity checks implemented

Related Skills

  • backend-development - Server-side payment handling
  • security - PCI compliance and secure handling
  • oauth - Customer authentication
  • api-architecture - Payment API design

Reference Resources