Overview
Automated invoice and receipt generation system: branded PDF invoices, tax-compliant formatting, itemized line items, automatic numbering, and email delivery. Supports both one-time payments and recurring subscriptions.
Use Cases
- SaaS billing: Monthly subscription invoices with tax breakdown
- E-commerce: Order receipts with shipping and tax details
- Consulting services: Professional invoices for client projects
- Compliance: Tax-compliant invoices for international customers (VAT, GST)
When to Use This Pattern
Use this pattern when you need to:
- Generate professional invoices for payments
- Comply with tax regulations (VAT/GST invoices required in many countries)
- Provide downloadable receipts to customers
- Maintain audit trail for accounting
- Handle invoice numbering and sequencing
- Support multiple currencies and tax jurisdictions
Pro Tips
Before you start implementing, read these carefully:
- Stripe already generates invoices - For subscriptions, use Stripe's built-in invoicing when possible
- Tax compliance varies by country - EU requires VAT number, tax breakdown, and specific formatting
- Invoice numbers must be sequential - Use database sequence or atomic counter
- Never regenerate invoices - Once issued, invoice content should be immutable (except void/credit note)
- Store PDFs permanently - Regulations often require 7-10 years retention
Implementation Phases
Phase 1: Database Schema
Track invoices with all required fields:
model Invoice {
id String @id @default(uuid())
invoiceNumber String @unique // INV-2025-0001
userId String
paymentId String? @unique
subscriptionId String?
// Invoice details
status String // draft, issued, paid, void, overdue
issuedAt DateTime @default(now())
dueAt DateTime?
paidAt DateTime?
// Amounts
subtotal Int // Amount in cents before tax
taxAmount Int // Tax amount in cents
total Int // Total amount in cents
currency String @default("usd")
// Tax information
taxRate Float? // Tax rate percentage (e.g., 20.0 for 20%)
taxType String? // VAT, GST, sales_tax, etc.
taxId String? // Customer's VAT/GST number
// Business details
businessName String?
businessAddress Json?
customerName String
customerEmail String
customerAddress Json?
// Line items
items Json // Array of line items
// Files
pdfUrl String? // S3/Cloudinary URL
pdfGeneratedAt DateTime?
// Metadata
notes String?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
payment Payment? @relation(fields: [paymentId], references: [id])
@@index([userId, status])
@@index([invoiceNumber])
@@index([issuedAt])
}
Line item structure (stored in items JSON field):
interface InvoiceLineItem {
description: string
quantity: number
unitAmount: number // In cents
taxRate?: number
total: number // quantity * unitAmount
}
// Example:
const items: InvoiceLineItem[] = [
{
description: 'Pro Plan - Monthly Subscription',
quantity: 1,
unitAmount: 4900,
taxRate: 20,
total: 4900,
},
{
description: 'Additional user seats (3)',
quantity: 3,
unitAmount: 1000,
taxRate: 20,
total: 3000,
},
]
Phase 2: Invoice Number Generation
Sequential invoice numbering:
// lib/invoice-number.ts
export async function generateInvoiceNumber(): Promise<string> {
const year = new Date().getFullYear()
const prefix = `INV-${year}-`
// Get last invoice number for current year
const lastInvoice = await prisma.invoice.findFirst({
where: {
invoiceNumber: {
startsWith: prefix,
},
},
orderBy: {
invoiceNumber: 'desc',
},
})
let nextNumber = 1
if (lastInvoice) {
const lastNumber = parseInt(lastInvoice.invoiceNumber.split('-').pop()!)
nextNumber = lastNumber + 1
}
// Pad with zeros: INV-2025-0001
const paddedNumber = nextNumber.toString().padStart(4, '0')
return `${prefix}${paddedNumber}`
}
// Alternative: Use database sequence (PostgreSQL)
// CREATE SEQUENCE invoice_number_seq START 1;
// SELECT nextval('invoice_number_seq');
Handle concurrent invoice creation:
// Use transaction to prevent duplicate numbers
export async function createInvoiceWithUniqueNumber(data: any) {
return await prisma.$transaction(async (tx) => {
const invoiceNumber = await generateInvoiceNumber()
// Check if number already exists (race condition)
const exists = await tx.invoice.findUnique({
where: { invoiceNumber },
})
if (exists) {
throw new Error('Invoice number collision, retry')
}
return await tx.invoice.create({
data: {
...data,
invoiceNumber,
},
})
})
}
Phase 3: PDF Generation with React-PDF
Install dependencies:
npm install @react-pdf/renderer
Create branded invoice template:
// lib/invoice-pdf.tsx
import {
Document,
Page,
Text,
View,
StyleSheet,
PDFDownloadLink,
pdf,
} from '@react-pdf/renderer'
const styles = StyleSheet.create({
page: {
padding: 40,
fontSize: 11,
fontFamily: 'Helvetica',
},
header: {
marginBottom: 30,
},
companyName: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 5,
},
invoiceTitle: {
fontSize: 18,
fontWeight: 'bold',
marginTop: 20,
marginBottom: 10,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 5,
},
table: {
marginTop: 20,
marginBottom: 20,
},
tableHeader: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000',
paddingBottom: 5,
marginBottom: 10,
fontWeight: 'bold',
},
tableRow: {
flexDirection: 'row',
paddingVertical: 5,
},
col1: { width: '50%' },
col2: { width: '15%', textAlign: 'right' },
col3: { width: '15%', textAlign: 'right' },
col4: { width: '20%', textAlign: 'right' },
totals: {
marginLeft: 'auto',
width: '40%',
marginTop: 20,
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 5,
},
grandTotal: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: '#000',
fontWeight: 'bold',
fontSize: 14,
},
footer: {
position: 'absolute',
bottom: 40,
left: 40,
right: 40,
fontSize: 9,
color: '#666',
textAlign: 'center',
},
})
interface InvoicePDFProps {
invoice: {
invoiceNumber: string
issuedAt: Date
dueAt?: Date
customerName: string
customerEmail: string
customerAddress?: any
businessName: string
businessAddress?: any
items: InvoiceLineItem[]
subtotal: number
taxAmount: number
taxRate?: number
taxType?: string
total: number
currency: string
}
}
export function InvoicePDF({ invoice }: InvoicePDFProps) {
const formatCurrency = (cents: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoice.currency.toUpperCase(),
}).format(cents / 100)
}
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.companyName}>{invoice.businessName}</Text>
{invoice.businessAddress && (
<>
<Text>{invoice.businessAddress.line1}</Text>
{invoice.businessAddress.line2 && (
<Text>{invoice.businessAddress.line2}</Text>
)}
<Text>
{invoice.businessAddress.city}, {invoice.businessAddress.state}{' '}
{invoice.businessAddress.postal_code}
</Text>
<Text>{invoice.businessAddress.country}</Text>
</>
)}
</View>
{/* Invoice title and details */}
<Text style={styles.invoiceTitle}>INVOICE</Text>
<View style={styles.row}>
<Text>Invoice Number: {invoice.invoiceNumber}</Text>
<Text>
Date: {new Date(invoice.issuedAt).toLocaleDateString()}
</Text>
</View>
{invoice.dueAt && (
<View style={styles.row}>
<Text>Due Date: {new Date(invoice.dueAt).toLocaleDateString()}</Text>
</View>
)}
{/* Bill to */}
<View style={{ marginTop: 30, marginBottom: 20 }}>
<Text style={{ fontWeight: 'bold', marginBottom: 5 }}>Bill To:</Text>
<Text>{invoice.customerName}</Text>
<Text>{invoice.customerEmail}</Text>
{invoice.customerAddress && (
<>
<Text>{invoice.customerAddress.line1}</Text>
{invoice.customerAddress.line2 && (
<Text>{invoice.customerAddress.line2}</Text>
)}
<Text>
{invoice.customerAddress.city}, {invoice.customerAddress.state}{' '}
{invoice.customerAddress.postal_code}
</Text>
</>
)}
</View>
{/* Line items table */}
<View style={styles.table}>
<View style={styles.tableHeader}>
<Text style={styles.col1}>Description</Text>
<Text style={styles.col2}>Qty</Text>
<Text style={styles.col3}>Price</Text>
<Text style={styles.col4}>Amount</Text>
</View>
{invoice.items.map((item, idx) => (
<View key={idx} style={styles.tableRow}>
<Text style={styles.col1}>{item.description}</Text>
<Text style={styles.col2}>{item.quantity}</Text>
<Text style={styles.col3}>{formatCurrency(item.unitAmount)}</Text>
<Text style={styles.col4}>{formatCurrency(item.total)}</Text>
</View>
))}
</View>
{/* Totals */}
<View style={styles.totals}>
<View style={styles.totalRow}>
<Text>Subtotal:</Text>
<Text>{formatCurrency(invoice.subtotal)}</Text>
</View>
{invoice.taxAmount > 0 && (
<View style={styles.totalRow}>
<Text>
{invoice.taxType || 'Tax'} ({invoice.taxRate}%):
</Text>
<Text>{formatCurrency(invoice.taxAmount)}</Text>
</View>
)}
<View style={styles.grandTotal}>
<Text>Total:</Text>
<Text>{formatCurrency(invoice.total)}</Text>
</View>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text>Thank you for your business!</Text>
<Text>For questions about this invoice, contact {invoice.customerEmail}</Text>
</View>
</Page>
</Document>
)
}
// Generate PDF buffer
export async function generateInvoicePDF(invoice: any): Promise<Buffer> {
const doc = <InvoicePDF invoice={invoice} />
const blob = await pdf(doc).toBlob()
const arrayBuffer = await blob.arrayBuffer()
return Buffer.from(arrayBuffer)
}
Phase 4: API Endpoint to Generate and Store Invoice
Create invoice API (app/api/invoices/create/route.ts):
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { generateInvoicePDF } from '@/lib/invoice-pdf'
import { uploadToS3 } from '@/lib/storage' // or Cloudinary, Vercel Blob
export async function POST(request: Request) {
const session = await getServerSession()
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { paymentId, subscriptionId } = await request.json()
const user = await prisma.user.findUnique({
where: { email: session.user.email },
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Fetch payment or subscription data
const payment = paymentId
? await prisma.payment.findUnique({ where: { id: paymentId } })
: null
// Generate invoice number
const invoiceNumber = await generateInvoiceNumber()
// Create invoice record
const invoice = await prisma.invoice.create({
data: {
invoiceNumber,
userId: user.id,
paymentId,
subscriptionId,
status: 'issued',
issuedAt: new Date(),
paidAt: payment?.status === 'succeeded' ? new Date() : null,
customerName: user.name || user.email,
customerEmail: user.email,
businessName: 'Your Company Inc.',
businessAddress: {
line1: '123 Business St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US',
},
items: [
{
description: 'Pro Plan - Monthly Subscription',
quantity: 1,
unitAmount: 4900,
total: 4900,
},
],
subtotal: 4900,
taxAmount: 0,
total: 4900,
currency: 'usd',
},
})
// Generate PDF
const pdfBuffer = await generateInvoicePDF(invoice)
// Upload to storage
const pdfUrl = await uploadToS3(
pdfBuffer,
`invoices/${invoiceNumber}.pdf`,
'application/pdf'
)
// Update invoice with PDF URL
await prisma.invoice.update({
where: { id: invoice.id },
data: {
pdfUrl,
pdfGeneratedAt: new Date(),
},
})
// Send invoice email
await sendInvoiceEmail(user.email, invoice, pdfUrl)
return NextResponse.json({
invoiceId: invoice.id,
invoiceNumber,
pdfUrl,
})
} catch (error) {
console.error('Invoice creation failed:', error)
return NextResponse.json(
{ error: 'Failed to create invoice' },
{ status: 500 }
)
}
}
Phase 5: Automated Invoice Generation via Webhooks
Generate invoices automatically on payment success:
// In webhook handler (app/api/webhooks/stripe/route.ts)
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
// ... update payment record ...
// Generate invoice
await fetch(`${process.env.APP_URL}/api/invoices/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paymentId: paymentIntent.id,
}),
})
}
async function handleInvoicePaid(invoice: Stripe.Invoice) {
// Stripe already generated invoice, fetch and store locally
const pdfUrl = invoice.invoice_pdf
await prisma.invoice.create({
data: {
invoiceNumber: invoice.number!,
stripeInvoiceId: invoice.id,
userId: invoice.metadata.userId,
status: 'paid',
issuedAt: new Date(invoice.created * 1000),
paidAt: new Date(),
pdfUrl, // Stripe's hosted PDF
total: invoice.total,
subtotal: invoice.subtotal,
taxAmount: invoice.tax || 0,
currency: invoice.currency,
// ... other fields
},
})
}
Edge Cases to Handle
Critical Edge Cases
Voided invoices:
- Customer disputes charge, need to void invoice
- Don't delete invoice (required for audit trail)
- Set status to 'void', issue credit note if needed
- Generate new invoice if re-charging
Credit notes and refunds:
- Partial or full refund issued
- Create separate credit note (negative invoice)
- Link credit note to original invoice
- Show both in customer's invoice history
Invoice amendments:
- Typo in customer name, need to fix
- NEVER modify issued invoice (immutable for compliance)
- Void original, issue corrected invoice
- Add note explaining void reason
Tax exemption:
- Customer provides valid tax exemption certificate
- Store tax exemption status on user record
- Set taxAmount to 0, add exemption note
- Include exemption certificate reference on invoice
Multiple currencies:
- Business operates in USD but customer pays in EUR
- Store both original currency and converted amount
- Show exchange rate and date on invoice
- Use Stripe's multi-currency support
Invoice disputes:
- Customer claims invoice incorrect
- Add dispute status to invoice
- Track dispute resolution (communication log)
- Update status once resolved
Storage failures:
- PDF generation succeeds but upload fails
- Retry upload with exponential backoff
- Store PDF generation status separately
- Queue for manual upload if all retries fail
Tech Stack Recommendations
Minimum Viable Stack
- PDF Generation: @react-pdf/renderer (React components to PDF)
- Storage: Vercel Blob or Cloudinary (simple, no S3 config)
- Email: Resend for invoice delivery
- Database: PostgreSQL with invoice table
Production-Grade Stack
- PDF Generation: @react-pdf/renderer + custom templates per brand
- Storage: S3 + CloudFront (CDN for fast downloads)
- Email: Resend + React Email for branded templates
- Database: Prisma + PostgreSQL with audit log
- Queue: Inngest for async PDF generation
- Compliance: Stripe Tax for automatic tax calculation
Full Implementation Prompt
Copy this prompt to use with Claude Code:
I need to implement an invoice generation system for my payment flows. Before we start, help me understand my requirements.
First, let's review my setup:
- Am I using Stripe? (If so, Stripe generates invoices for subscriptions automatically)
- What's my business entity info? (Company name, address, tax ID for invoices)
- What storage am I using? (S3, Cloudinary, Vercel Blob)
- Do I need tax compliance? (VAT/GST invoicing requirements)
- What's my invoice retention policy? (7-10 years typical for compliance)
Then let's discuss these business decisions:
- Invoice numbering format: INV-2025-0001 or custom?
- Payment terms: Due on receipt or Net 30?
- Tax handling: Manual rates or Stripe Tax?
- Invoice amendments: Allow edits or void + reissue only?
- Credit notes: How to handle refunds and disputes?
Then we'll implement in phases: Phase 1: Database schema for invoices Phase 2: Invoice number generation (sequential, unique) Phase 3: PDF generation with branded template Phase 4: Storage integration (upload PDFs) Phase 5: API endpoint for manual invoice creation Phase 6: Automated generation via webhooks Phase 7: Email delivery with PDF attachment
After implementation, let's test:
- Generate invoice for one-time payment
- Generate invoice for subscription renewal
- Test tax calculation (if applicable)
- Test PDF generation and download
- Test email delivery
- Test voiding an invoice
- Test credit note generation
Sound good? Let's start by reviewing your business info and deciding on invoice format.
Related Feature Specs
- Payment Flow - One-time payment invoicing
- Stripe Subscriptions - Recurring invoice generation
- Payment Webhooks - Automated invoice triggers
- Email System - Invoice email delivery
Success Criteria
You've successfully implemented this when:
ā Invoices generate automatically on payment ā Invoice numbers are sequential and unique ā PDF invoices are branded and professional ā Tax calculations are accurate (if applicable) ā PDFs are stored permanently and securely ā Customers receive invoice emails automatically ā Invoices are downloadable from account page ā Voided invoices handled correctly (audit trail)
Common Mistakes to Avoid
ā Non-sequential or duplicate invoice numbers ā Modifying issued invoices (break audit trail) ā Not storing PDFs permanently (compliance risk) ā Missing tax information (VAT/GST requirements) ā PDF generation blocking webhook response ā Not handling storage failures (retry logic) ā Forgetting invoice retention policy ā Not testing PDF rendering (broken layouts)
Implementation Checklist
Setup:
- Install @react-pdf/renderer
- Set up storage (S3, Cloudinary, Vercel Blob)
- Gather business information (name, address, tax ID)
- Decide on invoice numbering format
Database:
- Create Invoice model with all fields
- Add unique constraint on invoiceNumber
- Add indexes on userId, status, issuedAt
- Create database sequence (optional)
Invoice Numbering:
- Implement sequential number generation
- Add transaction to prevent duplicates
- Test concurrent invoice creation
- Handle year rollover (INV-2025-xxxx ā INV-2026-0001)
PDF Generation:
- Create InvoicePDF component with @react-pdf/renderer
- Add company branding (logo, colors)
- Format line items table
- Calculate and display totals, tax
- Add footer with payment terms
- Test PDF rendering with sample data
Storage:
- Set up storage credentials (S3, Cloudinary)
- Implement upload function
- Generate public URL for PDF access
- Add retry logic for upload failures
- Test file permissions (public read)
API Endpoints:
- Create invoice creation endpoint
- Add authentication check
- Generate invoice number
- Create invoice record in database
- Generate PDF
- Upload to storage
- Update database with PDF URL
- Send email with PDF attachment
Webhook Integration:
- Add invoice generation to payment success handler
- Handle subscription invoice events
- Queue PDF generation (don't block webhook)
- Test with Stripe test webhooks
Email Delivery:
- Create invoice email template
- Attach PDF or include download link
- Send on invoice creation
- Send payment reminders (if due date passed)
User Interface:
- Add invoices page to account settings
- List all invoices with status, date, amount
- Add download button for each invoice
- Show payment status (paid, pending, overdue)
Testing:
- Generate invoice for test payment
- Download PDF and verify formatting
- Test email delivery
- Test tax calculations (if applicable)
- Test invoice voiding
- Test concurrent number generation
- Test storage failures (retry logic)
Last Updated: 2025-12-04 Difficulty: Intermediate Estimated Time: 4-5 hours Prerequisites: Payment Flow, PDF basics, file storage knowledge
Need help with tax compliance? Book a consultation for international invoicing requirements and Stripe Tax setup.