Appearance
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
| Tier | Monthly Price | Storage | Support |
|---|---|---|---|
| Starter | $29 | 1 GB | |
| Professional | $99 | 10 GB | Priority Email |
| Business | $299 | 50 GB | Phone & Email |
| Enterprise | Custom | Unlimited | Dedicated 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=activeUpdate 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=trueUsage-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}/pdfPayment 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 subscriptionWebhooks
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
- Proration: Handle mid-cycle plan changes fairly
- Grace periods: Give customers time to update payment methods
- Clear communication: Send timely billing notifications
- Tax compliance: Automate tax calculation and reporting
- Security: PCI DSS compliance for payment data
- Transparency: Provide detailed usage breakdowns
- Flexibility: Offer multiple payment options
- Automation: Automate invoicing and dunning
- Analytics: Track key metrics (MRR, churn, LTV)
- 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
- Check payment method validity
- Verify sufficient funds
- Check for bank declines
- Review fraud detection rules
- Contact payment processor
Invoice Discrepancies
- Review usage records
- Check proration calculations
- Verify tax calculations
- Audit subscription changes
Next Steps
- API Keys - Manage API access
- Templates - Customize invoices
- Batch Billing - Process bulk billing
- Custom Pricing - Enterprise contracts
