Pricing and Billing Guide
Complete guide to implementing pricing models, usage tracking, billing systems, and payment integration
Learn how to implement comprehensive pricing and billing systems for your Services-as-Software, from simple per-use pricing to complex tiered subscriptions with usage tracking and automated invoicing.
Why Pricing and Billing Matters
Effective pricing and billing is critical for Services-as-Software success:
- Revenue Generation - Convert service value into sustainable revenue
- Customer Clarity - Transparent pricing builds trust and reduces friction
- Business Insights - Usage data drives product and business decisions
- Scalability - Automated billing enables growth without overhead
- Fair Value Exchange - Align pricing with customer value received
- Compliance - Meet tax, accounting, and regulatory requirements
Pricing Model Selection
Per-Use Pricing
Charge customers based on actual usage - ideal for unpredictable or variable workloads:
import $ from 'sdk.do'
const emailService = await $.Service.create({
name: 'Email Delivery Service',
pricing: {
model: 'per-use',
rate: 0.001, // $0.001 per email
unit: 'email',
currency: 'USD',
// Minimum charge
minimum: {
charge: 1.0, // Minimum $1
unit: 'transaction',
},
// Pricing tiers based on volume
tiers: [
{ from: 0, to: 1000, rate: 0.001 }, // First 1000: $0.001 each
{ from: 1000, to: 10000, rate: 0.0008 }, // Next 9000: $0.0008 each
{ from: 10000, to: null, rate: 0.0005 }, // Above 10000: $0.0005 each
],
},
})Subscription Pricing
Charge recurring fees for ongoing access - ideal for predictable revenue:
const analyticsService = await $.Service.create({
name: 'Business Analytics Platform',
pricing: {
model: 'subscription',
currency: 'USD',
plans: [
{
id: 'starter',
name: 'Starter',
price: 29.0,
interval: 'month',
limits: {
reports: 10,
dashboards: 3,
users: 5,
dataRetention: 30, // days
},
features: ['basic-analytics', 'email-reports', 'api-access'],
},
{
id: 'professional',
name: 'Professional',
price: 99.0,
interval: 'month',
limits: {
reports: 50,
dashboards: 15,
users: 25,
dataRetention: 90,
},
features: ['advanced-analytics', 'custom-dashboards', 'email-reports', 'api-access', 'white-label'],
},
{
id: 'enterprise',
name: 'Enterprise',
price: 499.0,
interval: 'month',
limits: {
reports: null, // unlimited
dashboards: null,
users: null,
dataRetention: 365,
},
features: [
'advanced-analytics',
'custom-dashboards',
'email-reports',
'api-access',
'white-label',
'priority-support',
'dedicated-account-manager',
'sla-guarantee',
],
},
],
// Annual discount
annualDiscount: 0.2, // 20% off annual plans
},
})Tiered Pricing
Combine usage-based pricing with volume discounts:
const apiService = await $.Service.create({
name: 'API Gateway Service',
pricing: {
model: 'tiered',
unit: 'request',
currency: 'USD',
tiers: [
{
name: 'First 1 million',
from: 0,
to: 1000000,
rate: 0.0001, // $0.0001 per request
fixedFee: 0,
},
{
name: 'Next 9 million',
from: 1000000,
to: 10000000,
rate: 0.00008,
fixedFee: 100, // $100 base + per-request
},
{
name: 'Above 10 million',
from: 10000000,
to: null,
rate: 0.00005,
fixedFee: 800,
},
],
// Calculate total cost
calculator: (usage) => {
let cost = 0
let remaining = usage
for (const tier of pricing.tiers) {
if (remaining <= 0) break
const tierSize = tier.to ? tier.to - tier.from : remaining
const unitsInTier = Math.min(remaining, tierSize)
cost += tier.fixedFee + unitsInTier * tier.rate
remaining -= unitsInTier
}
return cost
},
},
})Hybrid Pricing
Combine subscription base with usage overages:
const videoService = await $.Service.create({
name: 'Video Processing Service',
pricing: {
model: 'hybrid',
currency: 'USD',
// Base subscription
subscription: {
plans: [
{
id: 'basic',
name: 'Basic',
price: 49.0,
interval: 'month',
included: {
processingMinutes: 100,
storage: 10, // GB
bandwidth: 100, // GB
},
},
{
id: 'pro',
name: 'Pro',
price: 199.0,
interval: 'month',
included: {
processingMinutes: 500,
storage: 100,
bandwidth: 1000,
},
},
],
},
// Overage charges
overages: {
processingMinutes: {
rate: 0.1, // $0.10 per minute over limit
unit: 'minute',
},
storage: {
rate: 0.1, // $0.10 per GB per month over limit
unit: 'gb-month',
},
bandwidth: {
rate: 0.05, // $0.05 per GB over limit
unit: 'gb',
},
},
// Calculate bill
calculator: (plan, usage) => {
let cost = plan.price
// Calculate overages
for (const [resource, limit] of Object.entries(plan.included)) {
const used = usage[resource]
if (used > limit) {
const overage = used - limit
cost += overage * pricing.overages[resource].rate
}
}
return cost
},
},
})Value-Based Pricing
Price based on value delivered rather than resources consumed:
const leadGenService = await $.Service.create({
name: 'B2B Lead Generation Service',
pricing: {
model: 'value-based',
currency: 'USD',
// Price per qualified lead
perLead: {
base: 50.0,
// Adjust based on lead quality
qualityMultipliers: {
'decision-maker': 2.0, // 2x for decision makers
'high-intent': 1.5, // 1.5x for high intent
warm: 1.0, // Base rate
cold: 0.5, // 0.5x for cold leads
},
// Adjust based on industry
industryMultipliers: {
'enterprise-software': 2.5,
'financial-services': 2.0,
healthcare: 1.8,
retail: 1.0,
other: 0.8,
},
},
// Success-based pricing
successBased: {
enabled: true,
baseRate: 25.0, // Lower base rate
conversionBonus: 100.0, // Bonus when lead converts
conversionWindow: 90, // days to track conversion
},
// Calculate lead value
calculator: (lead) => {
let price = pricing.perLead.base
// Apply quality multiplier
if (lead.quality) {
price *= pricing.perLead.qualityMultipliers[lead.quality] || 1.0
}
// Apply industry multiplier
if (lead.industry) {
price *= pricing.perLead.industryMultipliers[lead.industry] || 1.0
}
return price
},
},
})Usage Tracking Implementation
Basic Usage Tracking
Track service usage for billing:
import { db, on, send } from 'sdk.do'
// Track usage when service executes
on($.ServiceExecution.complete, async (execution) => {
// Record usage
await db.create($.UsageRecord, {
customerId: execution.customerId,
serviceId: execution.serviceId,
timestamp: new Date(),
quantity: execution.quantity || 1,
unit: execution.service.pricing.unit,
metadata: {
requestId: execution.requestId,
duration: execution.duration,
inputSize: execution.inputSize,
outputSize: execution.outputSize,
},
})
// Update customer usage totals
await db.update($.Customer, {
where: { id: execution.customerId },
data: {
totalUsage: { increment: execution.quantity || 1 },
lastUsage: new Date(),
},
})
})Advanced Usage Tracking
Track multiple metrics and aggregate usage:
import { db } from 'sdk.do'
class UsageTracker {
async trackUsage(params: { customerId: string; serviceId: string; metrics: Record<string, number>; metadata?: Record<string, any> }) {
const { customerId, serviceId, metrics, metadata } = params
// Create usage record
const record = await db.create($.UsageRecord, {
customerId,
serviceId,
timestamp: new Date(),
metrics,
metadata,
})
// Update aggregated usage
await this.updateAggregates(customerId, serviceId, metrics)
// Check for limit violations
await this.checkLimits(customerId, serviceId, metrics)
return record
}
async updateAggregates(customerId: string, serviceId: string, metrics: Record<string, number>) {
const now = new Date()
const periodStart = new Date(now.getFullYear(), now.getMonth(), 1)
// Get or create period aggregate
let aggregate = await db.findOne($.UsageAggregate, {
where: {
customerId,
serviceId,
period: 'month',
periodStart,
},
})
if (!aggregate) {
aggregate = await db.create($.UsageAggregate, {
customerId,
serviceId,
period: 'month',
periodStart,
periodEnd: new Date(now.getFullYear(), now.getMonth() + 1, 0),
metrics: {},
})
}
// Update metrics
for (const [metric, value] of Object.entries(metrics)) {
aggregate.metrics[metric] = (aggregate.metrics[metric] || 0) + value
}
await db.update($.UsageAggregate, {
where: { id: aggregate.id },
data: { metrics: aggregate.metrics },
})
}
async checkLimits(customerId: string, serviceId: string, metrics: Record<string, number>) {
// Get customer's plan
const subscription = await db.findOne($.Subscription, {
where: { customerId, status: 'active' },
include: { plan: true },
})
if (!subscription?.plan?.limits) return
// Check current period usage
const aggregate = await db.findOne($.UsageAggregate, {
where: {
customerId,
serviceId,
period: 'month',
periodStart: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
},
})
// Check each limit
for (const [metric, limit] of Object.entries(subscription.plan.limits)) {
const usage = aggregate?.metrics[metric] || 0
// Approaching limit (80%)
if (usage >= limit * 0.8 && usage < limit) {
await send($.Alert.create, {
type: 'usage-warning',
customerId,
metric,
usage,
limit,
threshold: 0.8,
})
}
// Exceeded limit
if (usage >= limit) {
await send($.Alert.create, {
type: 'usage-exceeded',
customerId,
metric,
usage,
limit,
})
// Handle based on plan settings
if (subscription.plan.enforceLimits) {
throw new Error(`Usage limit exceeded for ${metric}: ${usage}/${limit}`)
}
}
}
}
async getUsage(customerId: string, period: { start: Date; end: Date }) {
return await db.list($.UsageRecord, {
where: {
customerId,
timestamp: {
gte: period.start,
lte: period.end,
},
},
orderBy: { timestamp: 'desc' },
})
}
async getAggregateUsage(customerId: string, serviceId: string, period: 'day' | 'month' | 'year') {
return await db.findOne($.UsageAggregate, {
where: {
customerId,
serviceId,
period,
periodStart: {
gte: this.getPeriodStart(period),
},
},
})
}
private getPeriodStart(period: 'day' | 'month' | 'year'): Date {
const now = new Date()
switch (period) {
case 'day':
return new Date(now.getFullYear(), now.getMonth(), now.getDate())
case 'month':
return new Date(now.getFullYear(), now.getMonth(), 1)
case 'year':
return new Date(now.getFullYear(), 0, 1)
}
}
}
// Usage
const tracker = new UsageTracker()
// Track usage
await tracker.trackUsage({
customerId: 'cust-123',
serviceId: 'svc-456',
metrics: {
requests: 1,
tokensUsed: 1500,
duration: 2500, // ms
bandwidth: 150000, // bytes
},
metadata: {
endpoint: '/api/generate',
model: 'gpt-5',
},
})
// Get usage
const usage = await tracker.getUsage('cust-123', {
start: new Date('2025-10-01'),
end: new Date('2025-10-31'),
})Billing Calculation Logic
Simple Billing Calculator
Calculate bills from usage:
import { db } from 'sdk.do'
class BillingCalculator {
async calculateBill(customerId: string, period: { start: Date; end: Date }) {
// Get customer's subscription
const subscription = await db.findOne($.Subscription, {
where: { customerId, status: 'active' },
include: { plan: true },
})
// Get usage for period
const usage = await db.list($.UsageRecord, {
where: {
customerId,
timestamp: { gte: period.start, lte: period.end },
},
})
let total = 0
// Base subscription fee
if (subscription) {
total += subscription.plan.price
}
// Calculate usage charges
for (const record of usage) {
const service = await db.findOne($.Service, {
where: { id: record.serviceId },
})
// Apply pricing model
const cost = this.calculateUsageCost(service, record)
total += cost
}
return {
customerId,
period,
subscription: subscription
? {
planId: subscription.plan.id,
planName: subscription.plan.name,
amount: subscription.plan.price,
}
: null,
usage: {
records: usage.length,
amount: total - (subscription?.plan.price || 0),
},
total,
currency: subscription?.plan.currency || 'USD',
}
}
private calculateUsageCost(service: any, record: any): number {
const pricing = service.pricing
switch (pricing.model) {
case 'per-use':
return this.calculatePerUse(pricing, record)
case 'tiered':
return this.calculateTiered(pricing, record)
default:
return 0
}
}
private calculatePerUse(pricing: any, record: any): number {
return (record.quantity || 1) * pricing.rate
}
private calculateTiered(pricing: any, record: any): number {
let cost = 0
let remaining = record.quantity || 1
for (const tier of pricing.tiers) {
if (remaining <= 0) break
const tierSize = tier.to ? tier.to - tier.from : remaining
const unitsInTier = Math.min(remaining, tierSize)
cost += unitsInTier * tier.rate
remaining -= unitsInTier
}
return cost
}
}
// Usage
const calculator = new BillingCalculator()
const bill = await calculator.calculateBill('cust-123', {
start: new Date('2025-10-01'),
end: new Date('2025-10-31'),
})
console.log(`Total: ${bill.currency} ${bill.total.toFixed(2)}`)Advanced Billing with Discounts and Credits
Handle complex billing scenarios:
class AdvancedBillingCalculator extends BillingCalculator {
async calculateBill(customerId: string, period: { start: Date; end: Date }) {
// Get base bill
const baseBill = await super.calculateBill(customerId, period)
// Apply discounts
const discounts = await this.calculateDiscounts(customerId, baseBill)
// Apply credits
const credits = await this.applyCredits(customerId, baseBill.total)
// Calculate taxes
const taxes = await this.calculateTaxes(customerId, baseBill.total - credits.applied)
return {
...baseBill,
discounts,
credits,
taxes,
finalTotal: baseBill.total - discounts.total - credits.applied + taxes.total,
}
}
private async calculateDiscounts(customerId: string, bill: any) {
const discounts = []
let total = 0
// Volume discount
if (bill.usage.records > 1000) {
const discount = {
type: 'volume',
description: 'High volume discount',
percentage: 0.1, // 10%
amount: bill.usage.amount * 0.1,
}
discounts.push(discount)
total += discount.amount
}
// Promotional discount
const promo = await db.findOne($.Promotion, {
where: {
customerId,
validFrom: { lte: new Date() },
validTo: { gte: new Date() },
active: true,
},
})
if (promo) {
const discount = {
type: 'promotion',
code: promo.code,
description: promo.description,
percentage: promo.percentage,
amount: bill.total * promo.percentage,
}
discounts.push(discount)
total += discount.amount
}
// Loyalty discount
const customer = await db.findOne($.Customer, {
where: { id: customerId },
})
if (customer.loyaltyTier) {
const discount = {
type: 'loyalty',
tier: customer.loyaltyTier,
description: `${customer.loyaltyTier} tier discount`,
percentage: this.getLoyaltyDiscount(customer.loyaltyTier),
amount: bill.total * this.getLoyaltyDiscount(customer.loyaltyTier),
}
discounts.push(discount)
total += discount.amount
}
return { discounts, total }
}
private async applyCredits(customerId: string, amount: number) {
const availableCredits = await db.findOne($.CustomerCredits, {
where: { customerId },
})
if (!availableCredits || availableCredits.balance <= 0) {
return { available: 0, applied: 0, remaining: 0 }
}
const applied = Math.min(availableCredits.balance, amount)
// Update credit balance
await db.update($.CustomerCredits, {
where: { customerId },
data: {
balance: { decrement: applied },
},
})
return {
available: availableCredits.balance,
applied,
remaining: availableCredits.balance - applied,
}
}
private async calculateTaxes(customerId: string, amount: number) {
const customer = await db.findOne($.Customer, {
where: { id: customerId },
include: { billingAddress: true },
})
const taxRate = await this.getTaxRate(customer.billingAddress)
return {
rate: taxRate,
total: amount * taxRate,
region: customer.billingAddress.region,
}
}
private async getTaxRate(address: any): Promise<number> {
// Simplified tax calculation
// In production, use a tax service like TaxJar or Avalara
const taxRates: Record<string, number> = {
CA: 0.0725, // California
NY: 0.08, // New York
TX: 0.0625, // Texas
// ... other states
}
return taxRates[address.state] || 0
}
private getLoyaltyDiscount(tier: string): number {
const discounts: Record<string, number> = {
bronze: 0.05,
silver: 0.1,
gold: 0.15,
platinum: 0.2,
}
return discounts[tier.toLowerCase()] || 0
}
}Invoice Generation
Basic Invoice Generator
Create professional invoices:
import { db, send } from 'sdk.do'
class InvoiceGenerator {
async generateInvoice(bill: any) {
// Create invoice record
const invoice = await db.create($.Invoice, {
customerId: bill.customerId,
number: await this.getNextInvoiceNumber(),
date: new Date(),
dueDate: this.calculateDueDate(),
period: {
start: bill.period.start,
end: bill.period.end,
},
items: this.buildLineItems(bill),
subtotal: bill.total,
discounts: bill.discounts?.total || 0,
credits: bill.credits?.applied || 0,
taxes: bill.taxes?.total || 0,
total: bill.finalTotal,
currency: bill.currency,
status: 'pending',
})
// Send invoice to customer
await send($.Email.send, {
to: bill.customer.email,
subject: `Invoice ${invoice.number} - ${bill.customer.name}`,
template: 'invoice',
data: {
invoice,
customer: bill.customer,
},
})
return invoice
}
private async getNextInvoiceNumber(): Promise<string> {
const lastInvoice = await db.findOne($.Invoice, {
orderBy: { number: 'desc' },
})
if (!lastInvoice) {
return 'INV-0001'
}
const lastNumber = parseInt(lastInvoice.number.split('-')[1])
return `INV-${String(lastNumber + 1).padStart(4, '0')}`
}
private calculateDueDate(): Date {
const dueDate = new Date()
dueDate.setDate(dueDate.getDate() + 30) // 30 days
return dueDate
}
private buildLineItems(bill: any): any[] {
const items = []
// Subscription item
if (bill.subscription) {
items.push({
description: `${bill.subscription.planName} Subscription`,
quantity: 1,
rate: bill.subscription.amount,
amount: bill.subscription.amount,
})
}
// Usage items
if (bill.usage.amount > 0) {
items.push({
description: 'Usage Charges',
quantity: bill.usage.records,
rate: bill.usage.amount / bill.usage.records,
amount: bill.usage.amount,
})
}
// Discount items
if (bill.discounts) {
for (const discount of bill.discounts.discounts) {
items.push({
description: discount.description,
quantity: 1,
rate: -discount.amount,
amount: -discount.amount,
})
}
}
// Tax items
if (bill.taxes) {
items.push({
description: `Tax (${bill.taxes.region})`,
quantity: 1,
rate: bill.taxes.total,
amount: bill.taxes.total,
})
}
return items
}
}PDF Invoice Generation
Generate PDF invoices:
class PDFInvoiceGenerator extends InvoiceGenerator {
async generateInvoice(bill: any) {
// Create base invoice
const invoice = await super.generateInvoice(bill)
// Generate PDF
const pdf = await this.createPDF(invoice, bill)
// Store PDF
const pdfUrl = await this.storePDF(invoice.number, pdf)
// Update invoice with PDF URL
await db.update($.Invoice, {
where: { id: invoice.id },
data: { pdfUrl },
})
return { ...invoice, pdfUrl }
}
private async createPDF(invoice: any, bill: any): Promise<Buffer> {
// Using a PDF generation library (e.g., puppeteer, pdfmake)
const html = this.generateInvoiceHTML(invoice, bill)
// Convert HTML to PDF (pseudo-code)
const pdf = await htmlToPDF(html)
return pdf
}
private generateInvoiceHTML(invoice: any, bill: any): string {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { text-align: center; margin-bottom: 30px; }
.invoice-details { margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.total { font-weight: bold; font-size: 1.2em; }
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
<p>Invoice Number: ${invoice.number}</p>
<p>Date: ${invoice.date.toLocaleDateString()}</p>
</div>
<div class="invoice-details">
<p><strong>Bill To:</strong></p>
<p>${bill.customer.name}</p>
<p>${bill.customer.email}</p>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Rate</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
${invoice.items
.map(
(item) => `
<tr>
<td>${item.description}</td>
<td>${item.quantity}</td>
<td>${invoice.currency} ${item.rate.toFixed(2)}</td>
<td>${invoice.currency} ${item.amount.toFixed(2)}</td>
</tr>
`
)
.join('')}
</tbody>
<tfoot>
<tr class="total">
<td colspan="3">Total</td>
<td>${invoice.currency} ${invoice.total.toFixed(2)}</td>
</tr>
</tfoot>
</table>
<p style="margin-top: 30px;">
<strong>Due Date:</strong> ${invoice.dueDate.toLocaleDateString()}
</p>
</body>
</html>
`
}
private async storePDF(invoiceNumber: string, pdf: Buffer): Promise<string> {
// Upload to storage (e.g., R2, S3)
const filename = `invoices/${invoiceNumber}.pdf`
const url = await uploadToStorage(filename, pdf)
return url
}
}Payment Integration
Stripe Integration
Integrate with Stripe for payment processing:
import Stripe from 'stripe'
import { db, on, send } from 'sdk.do'
class StripePaymentProcessor {
private stripe: Stripe
constructor(apiKey: string) {
this.stripe = new Stripe(apiKey, {
apiVersion: '2023-10-16',
})
}
async createCustomer(customer: any) {
const stripeCustomer = await this.stripe.customers.create({
email: customer.email,
name: customer.name,
metadata: {
customerId: customer.id,
},
})
// Store Stripe customer ID
await db.update($.Customer, {
where: { id: customer.id },
data: {
stripeCustomerId: stripeCustomer.id,
},
})
return stripeCustomer
}
async createSubscription(customerId: string, planId: string) {
// Get customer
const customer = await db.findOne($.Customer, {
where: { id: customerId },
})
// Get plan
const plan = await db.findOne($.Plan, {
where: { id: planId },
})
// Create Stripe subscription
const subscription = await this.stripe.subscriptions.create({
customer: customer.stripeCustomerId,
items: [{ price: plan.stripePriceId }],
metadata: {
customerId,
planId,
},
})
// Store subscription
await db.create($.Subscription, {
customerId,
planId,
stripeSubscriptionId: subscription.id,
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
})
return subscription
}
async chargeInvoice(invoiceId: string) {
// Get invoice
const invoice = await db.findOne($.Invoice, {
where: { id: invoiceId },
include: { customer: true },
})
// Create payment intent
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(invoice.total * 100), // Convert to cents
currency: invoice.currency.toLowerCase(),
customer: invoice.customer.stripeCustomerId,
description: `Invoice ${invoice.number}`,
metadata: {
invoiceId: invoice.id,
},
})
// Update invoice
await db.update($.Invoice, {
where: { id: invoiceId },
data: {
stripePaymentIntentId: paymentIntent.id,
status: 'processing',
},
})
return paymentIntent
}
async handleWebhook(event: Stripe.Event) {
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePaymentSuccess(event.data.object as Stripe.PaymentIntent)
break
case 'payment_intent.payment_failed':
await this.handlePaymentFailed(event.data.object as Stripe.PaymentIntent)
break
case 'invoice.payment_succeeded':
await this.handleInvoicePaymentSuccess(event.data.object as Stripe.Invoice)
break
case 'customer.subscription.updated':
await this.handleSubscriptionUpdate(event.data.object as Stripe.Subscription)
break
case 'customer.subscription.deleted':
await this.handleSubscriptionCanceled(event.data.object as Stripe.Subscription)
break
}
}
private async handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
const invoiceId = paymentIntent.metadata.invoiceId
await db.update($.Invoice, {
where: { id: invoiceId },
data: {
status: 'paid',
paidAt: new Date(),
},
})
await send($.Email.send, {
to: paymentIntent.receipt_email,
subject: 'Payment Confirmed',
template: 'payment-confirmation',
data: { invoiceId },
})
}
private async handlePaymentFailed(paymentIntent: Stripe.PaymentIntent) {
const invoiceId = paymentIntent.metadata.invoiceId
await db.update($.Invoice, {
where: { id: invoiceId },
data: {
status: 'failed',
},
})
await send($.Email.send, {
to: paymentIntent.receipt_email,
subject: 'Payment Failed',
template: 'payment-failed',
data: { invoiceId },
})
}
private async handleInvoicePaymentSuccess(invoice: Stripe.Invoice) {
// Update subscription status
if (invoice.subscription) {
await db.update($.Subscription, {
where: { stripeSubscriptionId: invoice.subscription as string },
data: {
status: 'active',
},
})
}
}
private async handleSubscriptionUpdate(subscription: Stripe.Subscription) {
await db.update($.Subscription, {
where: { stripeSubscriptionId: subscription.id },
data: {
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
})
}
private async handleSubscriptionCanceled(subscription: Stripe.Subscription) {
await db.update($.Subscription, {
where: { stripeSubscriptionId: subscription.id },
data: {
status: 'canceled',
canceledAt: new Date(),
},
})
}
}
// Setup webhook endpoint
on($.Webhook.received, async (webhook) => {
if (webhook.source !== 'stripe') return
const processor = new StripePaymentProcessor(process.env.STRIPE_SECRET_KEY!)
await processor.handleWebhook(webhook.data)
})Subscription Management
Subscription Lifecycle
Manage subscription lifecycle:
import { db, on, send } from 'sdk.do'
class SubscriptionManager {
async createSubscription(customerId: string, planId: string) {
// Create subscription
const subscription = await db.create($.Subscription, {
customerId,
planId,
status: 'active',
startDate: new Date(),
currentPeriodStart: new Date(),
currentPeriodEnd: this.calculatePeriodEnd('month'),
})
// Send confirmation
await send($.Email.send, {
customerId,
template: 'subscription-created',
data: { subscription },
})
return subscription
}
async upgradeSubscription(subscriptionId: string, newPlanId: string) {
const subscription = await db.findOne($.Subscription, {
where: { id: subscriptionId },
include: { plan: true, customer: true },
})
const newPlan = await db.findOne($.Plan, {
where: { id: newPlanId },
})
// Calculate prorated credit
const credit = this.calculateProratedCredit(subscription, newPlan)
// Apply credit
if (credit > 0) {
await db.update($.CustomerCredits, {
where: { customerId: subscription.customerId },
data: {
balance: { increment: credit },
},
})
}
// Update subscription
await db.update($.Subscription, {
where: { id: subscriptionId },
data: {
planId: newPlanId,
upgradedAt: new Date(),
},
})
return subscription
}
async downgradeSubscription(subscriptionId: string, newPlanId: string) {
// Schedule downgrade for end of period
await db.update($.Subscription, {
where: { id: subscriptionId },
data: {
scheduledPlanId: newPlanId,
scheduledChangeDate: this.calculatePeriodEnd('month'),
},
})
}
async cancelSubscription(subscriptionId: string, immediate = false) {
if (immediate) {
await db.update($.Subscription, {
where: { id: subscriptionId },
data: {
status: 'canceled',
canceledAt: new Date(),
},
})
} else {
// Cancel at end of period
await db.update($.Subscription, {
where: { id: subscriptionId },
data: {
cancelAtPeriodEnd: true,
},
})
}
}
async renewSubscription(subscriptionId: string) {
const subscription = await db.findOne($.Subscription, {
where: { id: subscriptionId },
include: { plan: true, customer: true },
})
// Calculate bill
const calculator = new AdvancedBillingCalculator()
const bill = await calculator.calculateBill(subscription.customerId, {
start: subscription.currentPeriodStart,
end: subscription.currentPeriodEnd,
})
// Generate invoice
const invoiceGenerator = new InvoiceGenerator()
const invoice = await invoiceGenerator.generateInvoice(bill)
// Charge payment method
const paymentProcessor = new StripePaymentProcessor(process.env.STRIPE_SECRET_KEY!)
await paymentProcessor.chargeInvoice(invoice.id)
// Update subscription period
await db.update($.Subscription, {
where: { id: subscriptionId },
data: {
currentPeriodStart: subscription.currentPeriodEnd,
currentPeriodEnd: this.calculatePeriodEnd(subscription.plan.interval),
},
})
}
private calculateProratedCredit(subscription: any, newPlan: any): number {
const now = new Date()
const periodTotal = subscription.currentPeriodEnd.getTime() - subscription.currentPeriodStart.getTime()
const periodRemaining = subscription.currentPeriodEnd.getTime() - now.getTime()
const percentRemaining = periodRemaining / periodTotal
const oldPlanCredit = subscription.plan.price * percentRemaining
const newPlanCharge = newPlan.price * percentRemaining
return Math.max(0, oldPlanCredit - newPlanCharge)
}
private calculatePeriodEnd(interval: 'month' | 'year'): Date {
const date = new Date()
if (interval === 'month') {
date.setMonth(date.getMonth() + 1)
} else {
date.setFullYear(date.getFullYear() + 1)
}
return date
}
}
// Automatic renewal
on($.Subscription.periodEnding, async (subscription) => {
if (subscription.cancelAtPeriodEnd) {
await new SubscriptionManager().cancelSubscription(subscription.id, true)
} else {
await new SubscriptionManager().renewSubscription(subscription.id)
}
})Complete Billing System Example
Putting it all together:
import { db, on, send } from 'sdk.do'
// Comprehensive billing orchestrator
class BillingOrchestrator {
private usageTracker = new UsageTracker()
private calculator = new AdvancedBillingCalculator()
private invoiceGenerator = new PDFInvoiceGenerator()
private paymentProcessor = new StripePaymentProcessor(process.env.STRIPE_SECRET_KEY!)
private subscriptionManager = new SubscriptionManager()
async processMonthlyBilling() {
// Get all active customers
const customers = await db.list($.Customer, {
where: { status: 'active' },
})
for (const customer of customers) {
await this.procesCustomerBilling(customer.id)
}
}
async processCustomerBilling(customerId: string) {
try {
// Calculate bill
const period = this.getCurrentBillingPeriod()
const bill = await this.calculator.calculateBill(customerId, period)
// Generate invoice
const invoice = await this.invoiceGenerator.generateInvoice(bill)
// Charge customer
await this.paymentProcessor.chargeInvoice(invoice.id)
// Send notification
await send($.Email.send, {
customerId,
template: 'monthly-invoice',
data: { invoice },
})
} catch (error) {
// Handle billing error
await send($.Alert.create, {
type: 'billing-error',
customerId,
error: error.message,
})
}
}
private getCurrentBillingPeriod() {
const now = new Date()
return {
start: new Date(now.getFullYear(), now.getMonth(), 1),
end: new Date(now.getFullYear(), now.getMonth() + 1, 0),
}
}
}
// Schedule monthly billing
on($.Schedule.monthly, async () => {
const orchestrator = new BillingOrchestrator()
await orchestrator.processMonthlyBilling()
})Best Practices
1. Transparent Pricing
Always be clear about pricing:
// ✅ Good: Clear pricing
pricing: {
model: 'per-use',
rate: 0.001,
unit: 'request',
minimum: 1.0,
description: '$0.001 per request, minimum $1.00 per transaction',
}
// ❌ Bad: Hidden fees
pricing: {
rate: 0.001,
// Missing: minimum charge, unit, description
}2. Accurate Usage Tracking
Track usage reliably:
// Always track usage atomically
// Use transactions to ensure consistency
// Include metadata for debugging3. Timely Invoicing
Send invoices promptly:
// Generate invoices immediately after billing period
// Send reminders before due date
// Handle failed payments gracefully4. Clear Communication
Keep customers informed:
// Send confirmation emails
// Provide detailed invoices
// Alert on usage thresholds
// Notify about billing changes5. Compliance
Follow regulations:
// Collect required tax information
// Calculate taxes correctly
// Retain records per regulations
// Provide clear terms of serviceNext Steps
- Implement Quality Assurance → - Ensure service quality
- Set Up Monitoring → - Track service performance
- Explore Monetization Strategies → - Advanced pricing models
- Learn Service Deployment → - Deploy to production