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"
)| Status | Description |
|---|---|
draft | Being assembled, not finalized |
open | Finalized and awaiting payment |
paid | Payment received |
void | Canceled before payment |
uncollectible | Payment 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
| Method | Path | Description |
|---|---|---|
POST | /ledger/invoices | Generate invoice manually |
GET | /ledger/invoices | List invoices |
GET | /ledger/invoices/{id} | Get invoice by ID |
GET | /ledger/invoices/{number} | Get invoice by number |
POST | /ledger/invoices/{id}/finalize | Finalize draft invoice |
POST | /ledger/invoices/{id}/void | Void an open invoice |
POST | /ledger/invoices/{id}/pay | Mark invoice as paid |
GET | /ledger/invoices/{id}/pdf | Download PDF invoice |