All Features / Invoice & Receipt Generation
4-5 hours
intermediate

Invoice & Receipt Generation

PDF invoices + tax compliance + branded receipts + email delivery

paymentscompliancepdf

Paste this into Claude Code to start implementing

šŸ”§Works With

This spec is compatible with:

The implementation prompt includes guidance for these tech stacks.

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:

  1. Stripe already generates invoices - For subscriptions, use Stripe's built-in invoicing when possible
  2. Tax compliance varies by country - EU requires VAT number, tax breakdown, and specific formatting
  3. Invoice numbers must be sequential - Use database sequence or atomic counter
  4. Never regenerate invoices - Once issued, invoice content should be immutable (except void/credit note)
  5. 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:

  1. Am I using Stripe? (If so, Stripe generates invoices for subscriptions automatically)
  2. What's my business entity info? (Company name, address, tax ID for invoices)
  3. What storage am I using? (S3, Cloudinary, Vercel Blob)
  4. Do I need tax compliance? (VAT/GST invoicing requirements)
  5. 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

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.

Need Implementation Help?

Get expert guidance on architecture, security, and best practices.

Book a Consultation
Invoice & Receipt Generation | Claude Code Implementation Guide | HashBuilds