Payment Integration Best Practices

Comprehensive guide to implementing secure and reliable payment processing with OpenDev.


Overview

This guide covers best practices for integrating payment providers including Stripe, Google Play Billing, Apple In-App Purchase, and WeChat Pay.


Architecture Patterns

Payment Flow Architecture

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │────▶│  Server  │────▶│ OpenDev  │────▶│ Provider │
│   App    │     │   API    │     │ Payment  │     │ (Stripe) │
└──────────┘     └──────────┘     └──────────┘     └──────────┘
     │                │                │                │
     │  1. Initiate   │                │                │
     │───────────────▶│  2. Create     │                │
     │                │───────────────▶│  3. Process    │
     │                │                │───────────────▶│
     │                │                │◀───────────────│
     │                │◀───────────────│  4. Confirm    │
     │◀───────────────│                │                │
     │  5. Complete   │                │                │

Webhook Architecture

┌──────────┐                    ┌──────────┐     ┌──────────┐
│ Provider │───────────────────▶│ OpenDev  │────▶│  Your    │
│ Webhook  │  1. Event sent     │ Webhook  │     │ Handler  │
└──────────┘                    └──────────┘     └──────────┘
                                     │               │
                                     │  2. Verify    │
                                     │  3. Forward   │
                                     │───────────────▶
                                     │◀──────────────│
                                     │  4. Process   │

Stripe Integration

Server-Side Implementation

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Create payment intent
async function createPayment(amount, currency, customerId) {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount, // In cents
    currency: currency,
    customer: customerId,
    metadata: {
      orderId: generateOrderId(),
      productId: 'product_123'
    }
  });
  
  return {
    clientSecret: paymentIntent.client_secret,
    paymentIntentId: paymentIntent.id
  };
}

Webhook Handling

// Webhook endpoint
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    logger.error('Webhook signature verification failed', err);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle event
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object);
      break;
    case 'payment_intent.payment_failed':
      await handlePaymentFailure(event.data.object);
      break;
    case 'charge.refunded':
      await handleRefund(event.data.object);
      break;
  }
  
  res.json({ received: true });
});

Idempotency

// Use idempotency keys for safe retries
async function createPaymentSafely(amount, currency, orderId) {
  const idempotencyKey = `payment_${orderId}`;
  
  return stripe.paymentIntents.create(
    {
      amount,
      currency,
      metadata: { orderId }
    },
    {
      idempotencyKey
    }
  );
}

Google Play Billing

Server-Side Verification

const { google } = require('googleapis');

async function verifyGooglePurchase(packageName, productId, purchaseToken) {
  const auth = new google.auth.GoogleAuth({
    keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY,
    scopes: ['https://www.googleapis.com/auth/androidpublisher']
  });
  
  const androidPublisher = google.androidpublisher({ version: 'v3', auth });
  
  try {
    const response = await androidPublisher.purchases.products.get({
      packageName,
      productId,
      token: purchaseToken
    });
    
    const purchase = response.data;
    
    // Verify purchase state
    if (purchase.purchaseState !== 0) {
      throw new Error('Purchase not completed');
    }
    
    // Check if already consumed
    if (purchase.consumptionState === 1) {
      throw new Error('Purchase already consumed');
    }
    
    return {
      valid: true,
      orderId: purchase.orderId,
      purchaseTime: purchase.purchaseTimeMillis
    };
  } catch (error) {
    logger.error('Google purchase verification failed', error);
    return { valid: false, error: error.message };
  }
}

Subscription Handling

async function verifyGoogleSubscription(packageName, subscriptionId, purchaseToken) {
  const androidPublisher = google.androidpublisher({ version: 'v3', auth });
  
  const response = await androidPublisher.purchases.subscriptions.get({
    packageName,
    subscriptionId,
    token: purchaseToken
  });
  
  const subscription = response.data;
  
  return {
    valid: true,
    expiryTime: new Date(parseInt(subscription.expiryTimeMillis)),
    autoRenewing: subscription.autoRenewing,
    cancelReason: subscription.cancelReason
  };
}

Apple In-App Purchase

Receipt Verification

const axios = require('axios');

async function verifyAppleReceipt(receiptData, isSandbox = false) {
  const url = isSandbox
    ? 'https://sandbox.itunes.apple.com/verifyReceipt'
    : 'https://buy.itunes.apple.com/verifyReceipt';
  
  try {
    const response = await axios.post(url, {
      'receipt-data': receiptData,
      'password': process.env.APPLE_SHARED_SECRET,
      'exclude-old-transactions': true
    });
    
    const result = response.data;
    
    // Status 21007 means sandbox receipt sent to production
    if (result.status === 21007) {
      return verifyAppleReceipt(receiptData, true);
    }
    
    if (result.status !== 0) {
      throw new Error(`Apple verification failed: ${result.status}`);
    }
    
    return {
      valid: true,
      receipt: result.receipt,
      latestReceiptInfo: result.latest_receipt_info
    };
  } catch (error) {
    logger.error('Apple receipt verification failed', error);
    return { valid: false, error: error.message };
  }
}

App Store Server Notifications

// Handle App Store Server Notifications (v2)
app.post('/webhooks/apple', async (req, res) => {
  const signedPayload = req.body.signedPayload;
  
  try {
    // Verify and decode the JWS
    const payload = await verifyAppleJWS(signedPayload);
    const notification = JSON.parse(payload);
    
    switch (notification.notificationType) {
      case 'DID_RENEW':
        await handleSubscriptionRenewal(notification);
        break;
      case 'DID_FAIL_TO_RENEW':
        await handleRenewalFailure(notification);
        break;
      case 'REFUND':
        await handleAppleRefund(notification);
        break;
      case 'EXPIRED':
        await handleSubscriptionExpired(notification);
        break;
    }
    
    res.status(200).send();
  } catch (error) {
    logger.error('Apple webhook processing failed', error);
    res.status(500).send();
  }
});

WeChat Pay

Payment Request

const crypto = require('crypto');

async function createWeChatPayment(orderId, amount, description, openId) {
  const params = {
    appid: process.env.WECHAT_APP_ID,
    mchid: process.env.WECHAT_MCH_ID,
    description,
    out_trade_no: orderId,
    notify_url: process.env.WECHAT_NOTIFY_URL,
    amount: {
      total: amount, // In cents (分)
      currency: 'CNY'
    },
    payer: {
      openid: openId
    }
  };
  
  const signature = generateWeChatSignature(params);
  
  const response = await axios.post(
    'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
    params,
    {
      headers: {
        'Authorization': `WECHATPAY2-SHA256-RSA2048 ${signature}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return response.data;
}

Signature Verification

function verifyWeChatSignature(timestamp, nonce, body, signature) {
  const message = `${timestamp}\n${nonce}\n${body}\n`;
  
  const verify = crypto.createVerify('RSA-SHA256');
  verify.update(message);
  
  return verify.verify(
    process.env.WECHAT_PLATFORM_PUBLIC_KEY,
    signature,
    'base64'
  );
}

app.post('/webhooks/wechat', async (req, res) => {
  const timestamp = req.headers['wechatpay-timestamp'];
  const nonce = req.headers['wechatpay-nonce'];
  const signature = req.headers['wechatpay-signature'];
  
  if (!verifyWeChatSignature(timestamp, nonce, JSON.stringify(req.body), signature)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process notification
  const notification = req.body;
  
  if (notification.event_type === 'TRANSACTION.SUCCESS') {
    await handleWeChatPaymentSuccess(notification.resource);
  }
  
  res.status(200).send();
});

Security Best Practices

Never Trust Client Data

// WRONG: Using client-provided amount
app.post('/api/payment', async (req, res) => {
  const { amount, productId } = req.body;
  // amount can be manipulated!
  await createPayment(amount, 'usd');
});

// CORRECT: Lookup price from server
app.post('/api/payment', async (req, res) => {
  const { productId } = req.body;
  const product = await getProduct(productId);
  await createPayment(product.price, product.currency);
});

Validate All Purchases

async function fulfillPurchase(purchase) {
  // 1. Verify with provider
  const verification = await verifyPurchase(purchase);
  if (!verification.valid) {
    throw new Error('Invalid purchase');
  }
  
  // 2. Check for duplicate fulfillment
  const existing = await db.purchases.findOne({
    where: { transactionId: verification.transactionId }
  });
  if (existing) {
    logger.warn('Duplicate fulfillment attempt', { transactionId });
    return existing;
  }
  
  // 3. Record and fulfill
  const record = await db.purchases.create({
    transactionId: verification.transactionId,
    userId: purchase.userId,
    productId: purchase.productId,
    amount: verification.amount,
    fulfilledAt: new Date()
  });
  
  // 4. Grant entitlement
  await grantEntitlement(purchase.userId, purchase.productId);
  
  return record;
}

Secure Webhook Endpoints

// Webhook security middleware
function webhookSecurity(provider) {
  return async (req, res, next) => {
    try {
      // 1. Verify signature
      if (!verifySignature(req, provider)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }
      
      // 2. Check timestamp (prevent replay attacks)
      const timestamp = getTimestamp(req, provider);
      if (Math.abs(Date.now() - timestamp) > 300000) { // 5 minutes
        return res.status(401).json({ error: 'Request too old' });
      }
      
      // 3. Rate limiting
      const ip = req.ip;
      if (await isRateLimited(ip, 'webhook')) {
        return res.status(429).json({ error: 'Too many requests' });
      }
      
      next();
    } catch (error) {
      logger.error('Webhook security check failed', error);
      res.status(500).json({ error: 'Internal error' });
    }
  };
}

Error Handling

Graceful Degradation

async function processPayment(paymentData) {
  const startTime = Date.now();
  
  try {
    const result = await paymentProvider.charge(paymentData);
    
    metrics.paymentDuration.observe(Date.now() - startTime);
    metrics.paymentSuccess.inc();
    
    return result;
  } catch (error) {
    metrics.paymentFailure.inc({ reason: error.code });
    
    // Categorize error
    if (error.code === 'card_declined') {
      throw new PaymentError('CARD_DECLINED', 'Your card was declined');
    } else if (error.code === 'expired_card') {
      throw new PaymentError('EXPIRED_CARD', 'Your card has expired');
    } else if (error.type === 'StripeConnectionError') {
      // Retry with exponential backoff
      return await retryPayment(paymentData);
    }
    
    throw new PaymentError('UNKNOWN', 'Payment failed. Please try again.');
  }
}

Refund Handling

async function processRefund(transactionId, reason) {
  const transaction = await db.transactions.findByPk(transactionId);
  
  if (!transaction) {
    throw new Error('Transaction not found');
  }
  
  if (transaction.refundedAt) {
    throw new Error('Already refunded');
  }
  
  // Process refund with provider
  const refund = await paymentProvider.refund(transaction.providerTransactionId);
  
  // Update local records
  await db.transactions.update(
    {
      refundedAt: new Date(),
      refundReason: reason,
      refundId: refund.id
    },
    { where: { id: transactionId } }
  );
  
  // Revoke entitlements
  await revokeEntitlement(transaction.userId, transaction.productId);
  
  // Notify user
  await notifyUser(transaction.userId, 'refund_processed', {
    amount: transaction.amount,
    currency: transaction.currency
  });
  
  return refund;
}

Monitoring & Analytics

Key Metrics

const metrics = {
  // Payment metrics
  paymentAttempts: new Counter('payment_attempts_total'),
  paymentSuccess: new Counter('payment_success_total'),
  paymentFailure: new Counter('payment_failure_total'),
  paymentAmount: new Histogram('payment_amount_dollars'),
  paymentDuration: new Histogram('payment_duration_ms'),
  
  // Subscription metrics
  activeSubscriptions: new Gauge('active_subscriptions'),
  subscriptionChurn: new Counter('subscription_churn_total'),
  
  // Webhook metrics
  webhookReceived: new Counter('webhook_received_total'),
  webhookProcessed: new Counter('webhook_processed_total'),
  webhookFailed: new Counter('webhook_failed_total')
};

Alerting

// Alert on payment failure spike
if (failureRate > 0.1) { // 10% failure rate
  alert.send({
    severity: 'critical',
    title: 'High Payment Failure Rate',
    message: `Payment failure rate is ${(failureRate * 100).toFixed(1)}%`
  });
}

// Alert on webhook processing delay
if (webhookProcessingTime > 5000) { // 5 seconds
  alert.send({
    severity: 'warning',
    title: 'Slow Webhook Processing',
    message: `Webhook processing time: ${webhookProcessingTime}ms`
  });
}

Last updated: January 2026