Skip to content

Billing System

Comprehensive billing and subscription management system for Pocketbook enterprise customers, including usage-based billing, invoicing, and payment processing.

Overview

The Pocketbook billing system provides:

  • Subscription management: Flexible subscription plans and tiers
  • Usage-based billing: Metered billing for API calls, storage, and more
  • Invoice generation: Automated invoice creation and delivery
  • Payment processing: Multiple payment methods and providers
  • Tax calculation: Automatic tax calculation and compliance
  • Billing analytics: Usage tracking and cost forecasting

Subscription Plans

Plan Tiers

TierMonthly PriceStorageSupport
Starter$291 GBEmail
Professional$9910 GBPriority Email
Business$29950 GBPhone & Email
EnterpriseCustomUnlimitedDedicated Support

Creating Subscriptions

typescript
// src/billing/subscription.ts
interface Subscription {
  id: string;
  customerId: string;
  planId: string;
  status: 'active' | 'canceled' | 'past_due' | 'trialing';
  currentPeriodStart: Date;
  currentPeriodEnd: Date;
  cancelAtPeriodEnd: boolean;
  trialEnd?: Date;
  metadata: Record<string, any>;
}

export async function createSubscription(
  customerId: string,
  planId: string,
  options?: SubscriptionOptions
): Promise<Subscription> {
  const subscription: Subscription = {
    id: crypto.randomUUID(),
    customerId,
    planId,
    status: options?.trial ? 'trialing' : 'active',
    currentPeriodStart: new Date(),
    currentPeriodEnd: calculatePeriodEnd(new Date(), 'month'),
    cancelAtPeriodEnd: false,
    trialEnd: options?.trial ? calculateTrialEnd(options.trialDays) : undefined,
    metadata: options?.metadata || {},
  };

  await db.subscriptions.create(subscription);

  // Send welcome email
  await sendSubscriptionConfirmation(subscription);

  return subscription;
}

Subscription API

Create Subscription

http
POST /api/v1/subscriptions
Content-Type: application/json

{
  "customerId": "cust_123",
  "planId": "plan_professional",
  "trialDays": 14,
  "paymentMethod": "card_456"
}

List Subscriptions

http
GET /api/v1/subscriptions?customerId=cust_123&status=active

Update Subscription

http
PATCH /api/v1/subscriptions/{subscription_id}
Content-Type: application/json

{
  "planId": "plan_business",
  "prorate": true
}

Cancel Subscription

http
DELETE /api/v1/subscriptions/{subscription_id}?at_period_end=true

Usage-Based Billing

Metered Billing

Track and bill for actual usage:

typescript
// src/billing/usage.ts
interface UsageRecord {
  id: string;
  subscriptionId: string;
  metricId: string;
  quantity: number;
  timestamp: Date;
  metadata?: Record<string, any>;
}

export async function recordUsage(
  subscriptionId: string,
  metricId: string,
  quantity: number
): Promise<void> {
  const record: UsageRecord = {
    id: crypto.randomUUID(),
    subscriptionId,
    metricId,
    quantity,
    timestamp: new Date(),
  };

  await db.usageRecords.create(record);

  // Update subscription usage totals
  await updateSubscriptionUsage(subscriptionId, metricId, quantity);
}

// Usage examples
await recordUsage('sub_123', 'api_calls', 1);
await recordUsage('sub_123', 'storage_gb', 0.5);
await recordUsage('sub_123', 'compute_hours', 2);

Billing Metrics

Define billable metrics:

typescript
interface BillingMetric {
  id: string;
  name: string;
  unit: string;
  unitAmount: number; // Price per unit in cents
  tiered?: TierPricing[];
}

const billingMetrics: BillingMetric[] = [
  {
    id: 'storage',
    name: 'Storage',
    unit: 'GB',
    unitAmount: 20, // $0.20 per GB
  },
  {
    id: 'bandwidth',
    name: 'Bandwidth',
    unit: 'GB',
    unitAmount: 10, // $0.10 per GB
  },
];

Tiered Pricing

Implement volume-based pricing:

typescript
interface TierPricing {
  upTo: number | null; // null = infinity
  unitAmount: number;
}

const tieredStoragePricing: TierPricing[] = [
  { upTo: 100, unitAmount: 20 },     // First 100 GB at $0.20/GB
  { upTo: 1000, unitAmount: 15 },    // 100-1000 GB at $0.15/GB
  { upTo: null, unitAmount: 10 },    // Above 1000 GB at $0.10/GB
];

export function calculateTieredPrice(
  quantity: number,
  tiers: TierPricing[]
): number {
  let total = 0;
  let remaining = quantity;
  let previousLimit = 0;

  for (const tier of tiers) {
    const tierLimit = tier.upTo || Infinity;
    const tierQuantity = Math.min(remaining, tierLimit - previousLimit);

    total += tierQuantity * tier.unitAmount;
    remaining -= tierQuantity;

    if (remaining <= 0) break;
    previousLimit = tierLimit;
  }

  return total;
}

Invoice Generation

Automated Invoicing

typescript
// src/billing/invoice.ts
interface Invoice {
  id: string;
  customerId: string;
  subscriptionId: string;
  number: string;
  status: 'draft' | 'open' | 'paid' | 'void' | 'uncollectible';
  subtotal: number;
  tax: number;
  total: number;
  currency: string;
  periodStart: Date;
  periodEnd: Date;
  dueDate: Date;
  paidAt?: Date;
  items: InvoiceItem[];
}

interface InvoiceItem {
  description: string;
  quantity: number;
  unitAmount: number;
  amount: number;
}

export async function generateInvoice(
  subscriptionId: string
): Promise<Invoice> {
  const subscription = await db.subscriptions.findById(subscriptionId);
  const customer = await db.customers.findById(subscription.customerId);

  // Calculate subscription amount
  const plan = await db.plans.findById(subscription.planId);
  const items: InvoiceItem[] = [
    {
      description: `${plan.name} - Subscription`,
      quantity: 1,
      unitAmount: plan.amount,
      amount: plan.amount,
    },
  ];

  // Add usage-based charges
  const usageCharges = await calculateUsageCharges(subscription.id);
  items.push(...usageCharges);

  // Calculate totals
  const subtotal = items.reduce((sum, item) => sum + item.amount, 0);
  const tax = await calculateTax(customer, subtotal);
  const total = subtotal + tax;

  const invoice: Invoice = {
    id: crypto.randomUUID(),
    customerId: customer.id,
    subscriptionId: subscription.id,
    number: generateInvoiceNumber(),
    status: 'open',
    subtotal,
    tax,
    total,
    currency: 'USD',
    periodStart: subscription.currentPeriodStart,
    periodEnd: subscription.currentPeriodEnd,
    dueDate: calculateDueDate(),
    items,
  };

  await db.invoices.create(invoice);

  // Send invoice to customer
  await sendInvoiceEmail(invoice);

  return invoice;
}

function generateInvoiceNumber(): string {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const sequence = crypto.randomBytes(4).toString('hex').toUpperCase();
  return `INV-${year}${month}-${sequence}`;
}

Invoice API

http
# Get invoice
GET /api/v1/invoices/{invoice_id}

# List invoices
GET /api/v1/invoices?customerId=cust_123&status=open

# Pay invoice
POST /api/v1/invoices/{invoice_id}/pay
Content-Type: application/json

{
  "paymentMethod": "card_456"
}

# Download invoice PDF
GET /api/v1/invoices/{invoice_id}/pdf

Payment Processing

Payment Methods

Support multiple payment methods:

typescript
interface PaymentMethod {
  id: string;
  customerId: string;
  type: 'card' | 'bank_account' | 'paypal' | 'crypto';
  isDefault: boolean;
  card?: {
    brand: string;
    last4: string;
    expMonth: number;
    expYear: number;
  };
  bankAccount?: {
    accountHolderName: string;
    last4: string;
    routingNumber: string;
  };
}

export async function addPaymentMethod(
  customerId: string,
  paymentDetails: any
): Promise<PaymentMethod> {
  // Tokenize payment method with payment processor
  const token = await paymentProcessor.tokenize(paymentDetails);

  const paymentMethod: PaymentMethod = {
    id: crypto.randomUUID(),
    customerId,
    type: paymentDetails.type,
    isDefault: false,
    ...token,
  };

  await db.paymentMethods.create(paymentMethod);

  return paymentMethod;
}

Process Payment

typescript
export async function processPayment(
  invoiceId: string,
  paymentMethodId: string
): Promise<Payment> {
  const invoice = await db.invoices.findById(invoiceId);
  const paymentMethod = await db.paymentMethods.findById(paymentMethodId);

  try {
    // Charge payment method
    const charge = await paymentProcessor.charge({
      amount: invoice.total,
      currency: invoice.currency,
      paymentMethod: paymentMethod.id,
      description: `Invoice ${invoice.number}`,
    });

    // Create payment record
    const payment: Payment = {
      id: crypto.randomUUID(),
      invoiceId: invoice.id,
      amount: invoice.total,
      status: 'succeeded',
      paymentMethodId: paymentMethod.id,
      chargeId: charge.id,
      createdAt: new Date(),
    };

    await db.payments.create(payment);

    // Update invoice status
    await db.invoices.update(invoice.id, {
      status: 'paid',
      paidAt: new Date(),
    });

    // Send receipt
    await sendPaymentReceipt(payment);

    return payment;
  } catch (error) {
    // Handle payment failure
    await handlePaymentFailure(invoice, error);
    throw error;
  }
}

Supported Payment Providers

  • Stripe: Credit cards, ACH, wallets
  • PayPal: PayPal accounts
  • Coinbase: Cryptocurrency payments
  • Bank Transfer: Wire transfers and ACH

Tax Calculation

Automatic Tax Calculation

typescript
// src/billing/tax.ts
import { TaxJar } from 'taxjar';

const taxjar = new TaxJar({
  apiKey: process.env.TAXJAR_API_KEY,
});

export async function calculateTax(
  customer: Customer,
  amount: number
): Promise<number> {
  try {
    const tax = await taxjar.taxForOrder({
      from_country: 'US',
      from_zip: '94025',
      from_state: 'CA',
      to_country: customer.country,
      to_zip: customer.zip,
      to_state: customer.state,
      amount,
      shipping: 0,
    });

    return tax.tax.amount_to_collect * 100; // Convert to cents
  } catch (error) {
    console.error('Tax calculation failed:', error);
    return 0;
  }
}

Tax Compliance

  • Sales Tax: Automatic calculation for US states
  • VAT: EU VAT handling with MOSS
  • GST: Support for Australia, Canada, India
  • Tax Exemptions: Handle tax-exempt customers

Billing Analytics

Revenue Metrics

typescript
// src/billing/analytics.ts
export async function getRevenueMetrics(
  startDate: Date,
  endDate: Date
): Promise<RevenueMetrics> {
  const payments = await db.payments.find({
    createdAt: { $gte: startDate, $lte: endDate },
    status: 'succeeded',
  });

  const subscriptions = await db.subscriptions.find({
    status: 'active',
  });

  const totalRevenue = payments.reduce((sum, p) => sum + p.amount, 0);
  const avgRevenuePerCustomer = totalRevenue / subscriptions.length;

  return {
    totalRevenue,
    avgRevenuePerCustomer,
    activeSubscriptions: subscriptions.length,
    churnRate: await calculateChurnRate(startDate, endDate),
    mrr: await calculateMRR(),
    arr: await calculateARR(),
  };
}

async function calculateMRR(): Promise<number> {
  const activeSubscriptions = await db.subscriptions.find({
    status: 'active',
  });

  let mrr = 0;

  for (const sub of activeSubscriptions) {
    const plan = await db.plans.findById(sub.planId);

    // Normalize to monthly
    if (plan.interval === 'month') {
      mrr += plan.amount;
    } else if (plan.interval === 'year') {
      mrr += plan.amount / 12;
    }
  }

  return mrr;
}

Usage Analytics

typescript
export async function getUsageAnalytics(
  subscriptionId: string,
  metricId: string,
  period: 'day' | 'week' | 'month'
): Promise<UsageData[]> {
  const startDate = getStartDate(period);

  const usage = await db.usageRecords.aggregate([
    {
      $match: {
        subscriptionId,
        metricId,
        timestamp: { $gte: startDate },
      },
    },
    {
      $group: {
        _id: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } },
        total: { $sum: '$quantity' },
      },
    },
    {
      $sort: { _id: 1 },
    },
  ]);

  return usage;
}

Dunning Management

Failed Payment Handling

typescript
// src/billing/dunning.ts
export async function handleFailedPayment(
  invoice: Invoice,
  attempt: number = 1
): Promise<void> {
  const MAX_ATTEMPTS = 3;

  if (attempt >= MAX_ATTEMPTS) {
    // Mark subscription as past_due
    await db.subscriptions.update(invoice.subscriptionId, {
      status: 'past_due',
    });

    // Send final notice
    await sendFinalPaymentNotice(invoice);

    return;
  }

  // Send payment failure notice
  await sendPaymentFailureNotice(invoice, attempt);

  // Schedule retry
  const retryDelay = calculateRetryDelay(attempt);
  setTimeout(async () => {
    try {
      await retryPayment(invoice.id);
    } catch (error) {
      await handleFailedPayment(invoice, attempt + 1);
    }
  }, retryDelay);
}

function calculateRetryDelay(attempt: number): number {
  // Exponential backoff: 1 day, 3 days, 7 days
  const delays = [
    1 * 24 * 60 * 60 * 1000,
    3 * 24 * 60 * 60 * 1000,
    7 * 24 * 60 * 60 * 1000,
  ];

  return delays[attempt - 1] || delays[delays.length - 1];
}

Customer Portal

Self-Service Billing

Allow customers to manage their billing:

typescript
// Generate customer portal URL
export async function generatePortalUrl(
  customerId: string
): Promise<string> {
  const session = await createPortalSession(customerId);

  return `${process.env.PORTAL_URL}/billing?session=${session.id}`;
}

// Customer portal features:
// - View invoices
// - Update payment methods
// - Change subscription plan
// - Download receipts
// - Update billing details
// - Cancel subscription

Webhooks

Billing Events

typescript
// Billing webhook events
export const BILLING_EVENTS = [
  'invoice.created',
  'invoice.paid',
  'invoice.payment_failed',
  'subscription.created',
  'subscription.updated',
  'subscription.canceled',
  'payment.succeeded',
  'payment.failed',
  'customer.subscription.trial_will_end',
];

// Example webhook payload
const webhookPayload = {
  type: 'invoice.paid',
  data: {
    invoiceId: 'inv_123',
    amount: 9999,
    customerId: 'cust_456',
  },
  created: '2024-01-15T10:30:00Z',
};

Best Practices

  1. Proration: Handle mid-cycle plan changes fairly
  2. Grace periods: Give customers time to update payment methods
  3. Clear communication: Send timely billing notifications
  4. Tax compliance: Automate tax calculation and reporting
  5. Security: PCI DSS compliance for payment data
  6. Transparency: Provide detailed usage breakdowns
  7. Flexibility: Offer multiple payment options
  8. Automation: Automate invoicing and dunning
  9. Analytics: Track key metrics (MRR, churn, LTV)
  10. Testing: Test billing flows thoroughly

Compliance

PCI DSS Compliance

  • Never store raw card data
  • Use tokenization
  • Secure transmission (TLS 1.2+)
  • Regular security audits
  • Access controls

SOC 2 Compliance

  • Audit logging
  • Data encryption
  • Access management
  • Incident response

Troubleshooting

Failed Payments

  1. Check payment method validity
  2. Verify sufficient funds
  3. Check for bank declines
  4. Review fraud detection rules
  5. Contact payment processor

Invoice Discrepancies

  1. Review usage records
  2. Check proration calculations
  3. Verify tax calculations
  4. Audit subscription changes

Next Steps

Resources

Released under the MIT License.