Back to Home

Payments & GST

Processor abstraction for Stripe and Razorpay with auto-routing, webhooks, and GST invoicing.

PropelKit uses a processor abstraction that lets you switch between Stripe and Razorpay (or use both with auto-routing) without changing your application code.

Architecture

Terminal
Your Code
    |
    v
getPaymentProcessor(currency?)  <-- Factory function
    |
    |--- currency === 'INR' && razorpay available → RazorpayProcessor
    |--- currency === 'USD' && stripe available   → StripeProcessor
    |--- fallback to configured default
    v
PaymentProcessor interface
    |
    |-- createCheckoutSession()
    |-- cancelSubscription()
    |-- upgradeSubscription()
    |-- fetchCustomerPortalUrl()  (Stripe only)
    |-- handleWebhook()

Configuration

Set in src/config/features.ts:

TypeScript
paymentProcessor: 'stripe'    // International (USD, EUR, GBP)
paymentProcessor: 'razorpay'  // India (INR, UPI)
paymentProcessor: 'both'      // Auto-routes: INR → Razorpay, USD → Stripe

Stripe

Checkout flow:

  • Server creates a Checkout Session with processor.createCheckoutSession()
  • User is redirected to checkout.stripe.com
  • After payment, Stripe calls your webhook
  • Webhook handler creates/updates subscription in database
  • User is redirected to success page

Customer Portal

Stripe supports self-service billing management. Users can update payment methods, cancel subscriptions, and view invoices through Stripe's hosted portal.

TypeScript
const portalUrl = await processor.fetchCustomerPortalUrl({ tenantContext });
redirect(portalUrl);

Stripe Webhook Events

  • checkout.session.completed — Payment successful
  • invoice.paid — Recurring charge received
  • customer.subscription.updated — Plan changed
  • customer.subscription.deleted — Subscription cancelled
  • payment_intent.payment_failed — Payment failed

Razorpay

Checkout flow:

  • Server creates a subscription with processor.createCheckoutSession()
  • Returns razorpaySubscriptionId and razorpayKey
  • Client opens Razorpay checkout widget
  • User pays (supports UPI, cards, net banking, wallets)
  • Razorpay calls your webhook
  • Webhook handler creates/updates subscription in database

Razorpay Webhook Events

  • payment.authorized / payment.captured — Payment received
  • payment.failed — Payment failed
  • subscription.activated / subscription.charged — Subscription events
  • subscription.cancelled / subscription.completed — Subscription ended

Customer Storage

Payment processor customer IDs are stored on profiles (single-user) or organizations (multi-tenant):

SQL
processor_customer_id   -- The Stripe/Razorpay customer ID
payment_processor       -- 'stripe' | 'razorpay'

GST & Invoicing

India-compliant GST calculation and PDF invoice generation. Enable with features.gst: true.

GST Calculation

GST calculation example
TypeScript
import { calculateGST, validateGSTIN } from '@/lib/gst/gst-calculator';

const result = calculateGST(100000, {  // Amount in paise
  gstin: customerGstin,
  billingStateCode: 'KA',
});

// Returns:
// {
//   baseAmount: 100000,
//   gstAmount: 18000,
//   totalAmount: 118000,
//   cgst: 9000,       // Intra-state (same state as company)
//   sgst: 9000,       // Intra-state
//   igst: null,        // Inter-state would be 18000 here
//   isInterState: false,
// }

// Validate GSTIN format
validateGSTIN('27AAAPA5055K1Z5');  // true

GST Rules

  • Intra-state (customer in same state as company): CGST 9% + SGST 9%
  • Inter-state (customer in different state): IGST 18%
  • State code is extracted from the first 2 digits of the GSTIN

Invoice Generation

Invoice generation
TypeScript
import { generateInvoicePDF } from '@/lib/gst/invoice-generator';

const pdfBuffer = await generateInvoicePDF({
  invoiceNumber: 'INV-2601-ABC123',
  invoiceDate: new Date(),
  customerName: 'John Doe',
  customerEmail: 'john@example.com',
  customerGSTIN: '27AAAPA5055K1Z5',
  items: [{ description: 'Pro Plan', quantity: 1, unitPrice: 49900, amount: 49900 }],
  baseAmount: 49900,
});

Company GST information is read from brand.company in src/config/brand.ts.