Ledger

Invoice Generation

Automated billing calculations from usage, subscriptions, and pricing rules.

Invoice Generation transforms subscription and usage data into detailed invoices with line items, taxes, and payment instructions. It runs automatically at billing period boundaries and supports manual generation for prorations and adjustments.

Structure

type Invoice struct {
    ledger.Entity
    ID             id.InvoiceID
    SubscriptionID id.SubscriptionID
    CustomerID     id.CustomerID
    TenantID       string
    AppID          string
    Number         string // Human-readable: INV-2024-001234
    Status         InvoiceStatus
    PeriodStart    time.Time
    PeriodEnd      time.Time
    LineItems      []LineItem
    Subtotal       money.Money
    Tax            money.Money
    Total          money.Money
    AmountDue      money.Money
    DueDate        time.Time
    PaidAt         *time.Time
    Metadata       map[string]any
}

Invoice lifecycle

type InvoiceStatus string

const (
    StatusDraft    InvoiceStatus = "draft"
    StatusOpen     InvoiceStatus = "open"
    StatusPaid     InvoiceStatus = "paid"
    StatusVoid     InvoiceStatus = "void"
    StatusUncollectible InvoiceStatus = "uncollectible"
)
StatusDescription
draftBeing assembled, not finalized
openFinalized and awaiting payment
paidPayment received
voidCanceled before payment
uncollectiblePayment collection failed permanently

Line items

Each invoice contains multiple line items:

type LineItem struct {
    ID          string
    Type        LineItemType
    Description string
    Quantity    float64
    UnitPrice   money.Money
    Amount      money.Money
    Period      *Period
    Metadata    map[string]any
}

type LineItemType string

const (
    LineItemSubscription LineItemType = "subscription"
    LineItemUsage        LineItemType = "usage"
    LineItemProration    LineItemType = "proration"
    LineItemOneTime      LineItemType = "one_time"
    LineItemTax          LineItemType = "tax"
    LineItemDiscount     LineItemType = "discount"
)

Example: Monthly invoice with usage

LineItems: []invoice.LineItem{
    {
        Type:        invoice.LineItemSubscription,
        Description: "Starter Plan - Monthly",
        Quantity:    1,
        UnitPrice:   money.Money{Amount: 2999, Currency: "USD"},
        Amount:      money.Money{Amount: 2999, Currency: "USD"},
        Period:      &Period{Start: periodStart, End: periodEnd},
    },
    {
        Type:        invoice.LineItemUsage,
        Description: "API Calls (10,000 - 15,000)",
        Quantity:    5000,
        UnitPrice:   money.Money{Amount: 10, Currency: "USD"}, // $0.01
        Amount:      money.Money{Amount: 50000, Currency: "USD"}, // $500
        Period:      &Period{Start: periodStart, End: periodEnd},
    },
    {
        Type:        invoice.LineItemTax,
        Description: "Sales Tax (8.5%)",
        Quantity:    1,
        Amount:      money.Money{Amount: 28491, Currency: "USD"}, // $284.91
    },
}

Automatic generation

Invoices are generated automatically at billing period boundaries:

// Background job runs daily
func generateDueInvoices(ctx context.Context) {
    subs, _ := store.GetSubscriptionsDueForBilling(ctx, time.Now())

    for _, sub := range subs {
        invoice, err := generator.Generate(ctx, &generator.Request{
            SubscriptionID: sub.ID,
            PeriodStart:    sub.CurrentPeriodStart,
            PeriodEnd:      sub.CurrentPeriodEnd,
        })

        if err != nil {
            log.Error("invoice generation failed", "sub", sub.ID, "error", err)
            continue
        }

        // Finalize and send to payment processor
        invoice.Status = invoice.StatusOpen
        store.UpdateInvoice(ctx, invoice)

        payment.CollectPayment(ctx, invoice)
    }
}

Manual generation

Generate invoices manually for upgrades, downgrades, or adjustments:

// Generate prorated invoice for plan change
invoice, err := generator.Generate(ctx, &generator.Request{
    SubscriptionID: subscription.ID,
    PeriodStart:    changeDate,
    PeriodEnd:      subscription.CurrentPeriodEnd,
    Type:           generator.TypeProration,
    Reason:         "Plan upgrade: Starter → Pro",
})

Graduated pricing calculation

For plans with graduated tiers, line items are split:

// Plan: First 5K free, next 5K at $0.01, rest at $0.005
// Usage: 15,000 calls

LineItems: []invoice.LineItem{
    {
        Description: "API Calls (0 - 5,000)",
        Quantity:    5000,
        UnitPrice:   money.Money{Amount: 0, Currency: "USD"},
        Amount:      money.Money{Amount: 0, Currency: "USD"},
    },
    {
        Description: "API Calls (5,001 - 10,000)",
        Quantity:    5000,
        UnitPrice:   money.Money{Amount: 10, Currency: "USD"},
        Amount:      money.Money{Amount: 50000, Currency: "USD"}, // $500
    },
    {
        Description: "API Calls (10,001 - 15,000)",
        Quantity:    5000,
        UnitPrice:   money.Money{Amount: 5, Currency: "USD"},
        Amount:      money.Money{Amount: 25000, Currency: "USD"}, // $250
    },
}

Proration

When a subscription changes mid-period, generate a prorated invoice:

// Customer upgrades from Starter ($29.99) to Pro ($99.99) on day 15 of 30
unusedDays := 15
proratedCredit := (2999 * unusedDays) / 30  // -$14.995
proratedCharge := (9999 * unusedDays) / 30  // +$49.995
netCharge := proratedCharge - proratedCredit // $35

LineItems: []invoice.LineItem{
    {
        Type:        invoice.LineItemProration,
        Description: "Unused time on Starter Plan",
        Amount:      money.Money{Amount: -1500, Currency: "USD"}, // -$15.00
    },
    {
        Type:        invoice.LineItemProration,
        Description: "Pro Plan (15 days)",
        Amount:      money.Money{Amount: 5000, Currency: "USD"}, // $50.00
    },
}

Tax calculation

Taxes are computed based on customer location and product taxability:

type TaxCalculator interface {
    Calculate(ctx context.Context, invoice *Invoice) (money.Money, error)
}

// Stripe Tax integration
taxCalculator := tax.NewStripeTax(stripeClient)
taxAmount, err := taxCalculator.Calculate(ctx, invoice)

invoice.Tax = taxAmount
invoice.Total = invoice.Subtotal.Add(taxAmount)

Discounts and credits

Apply discounts or account credits:

LineItems: []invoice.LineItem{
    {
        Type:        invoice.LineItemSubscription,
        Description: "Pro Plan - Monthly",
        Amount:      money.Money{Amount: 9999, Currency: "USD"},
    },
    {
        Type:        invoice.LineItemDiscount,
        Description: "20% off for 3 months",
        Amount:      money.Money{Amount: -2000, Currency: "USD"},
    },
}

Payment collection

After generating an invoice, initiate payment:

invoice.Status = invoice.StatusOpen
store.CreateInvoice(ctx, invoice)

// Attempt payment collection
payment, err := processor.Charge(ctx, &payment.ChargeRequest{
    InvoiceID:        invoice.ID,
    CustomerID:       invoice.CustomerID,
    Amount:           invoice.Total,
    PaymentMethodID:  customer.DefaultPaymentMethod,
})

if err != nil {
    // Payment failed - mark past due
    subscription.Status = subscription.StatusPastDue
} else {
    // Payment succeeded
    invoice.Status = invoice.StatusPaid
    invoice.PaidAt = &payment.CompletedAt
}

Store interface

type Store interface {
    CreateInvoice(ctx context.Context, inv *Invoice) error
    GetInvoice(ctx context.Context, invID id.InvoiceID) (*Invoice, error)
    GetInvoiceByNumber(ctx context.Context, number string) (*Invoice, error)
    GetInvoicesByCustomer(ctx context.Context, customerID id.CustomerID) ([]*Invoice, error)
    GetInvoicesBySubscription(ctx context.Context, subID id.SubscriptionID) ([]*Invoice, error)
    UpdateInvoice(ctx context.Context, inv *Invoice) error
    VoidInvoice(ctx context.Context, invID id.InvoiceID) error
    ListInvoices(ctx context.Context, filter *ListFilter) ([]*Invoice, error)
}

API routes

MethodPathDescription
POST/ledger/invoicesGenerate invoice manually
GET/ledger/invoicesList invoices
GET/ledger/invoices/{id}Get invoice by ID
GET/ledger/invoices/{number}Get invoice by number
POST/ledger/invoices/{id}/finalizeFinalize draft invoice
POST/ledger/invoices/{id}/voidVoid an open invoice
POST/ledger/invoices/{id}/payMark invoice as paid
GET/ledger/invoices/{id}/pdfDownload PDF invoice

On this page