Ledger

Customer Management

Manage customer profiles, billing addresses, and payment methods.

A Customer is the central entity in Ledger's billing system. It represents an individual or organization that subscribes to plans, generates usage, receives invoices, and makes payments.

Structure

type Customer struct {
    ledger.Entity
    ID                    id.CustomerID
    TenantID              string
    AppID                 string
    Name                  string
    Email                 string
    BillingAddress        *Address
    DefaultPaymentMethod  string
    Balance               money.Money
    Currency              string
    TaxIDs                []TaxID
    Metadata              map[string]any
}

Creating customers

customer := &customer.Customer{
    ID:       id.NewCustomerID(),
    TenantID: "acme-corp",
    AppID:    "saas-platform",
    Name:     "Alice Johnson",
    Email:    "alice@example.com",
    BillingAddress: &customer.Address{
        Line1:      "123 Main St",
        Line2:      "Suite 100",
        City:       "San Francisco",
        State:      "CA",
        PostalCode: "94105",
        Country:    "US",
    },
    Currency: "USD",
    Balance:  money.Money{Amount: 0, Currency: "USD"},
}

if err := store.CreateCustomer(ctx, customer); err != nil {
    log.Fatal(err)
}

Billing address

The billing address determines tax jurisdiction and payment routing:

type Address struct {
    Line1      string
    Line2      string
    City       string
    State      string
    PostalCode string
    Country    string // ISO 3166-1 alpha-2
}

Tax rates are calculated based on Country, State, and PostalCode:

taxRate, err := tax.GetRate(ctx, customer.BillingAddress)
invoice.Tax = invoice.Subtotal.Multiply(taxRate)

Payment methods

Customers can have multiple payment methods, with one set as default:

// Add a card via Stripe
method, err := processor.CreatePaymentMethod(ctx, customer.ID, stripeToken)

// Set as default
customer.DefaultPaymentMethod = method.ID
store.UpdateCustomer(ctx, customer)

// List all methods
methods, err := store.GetPaymentMethods(ctx, customer.ID)

When an invoice is generated, Ledger attempts to charge the default payment method automatically.

Customer balance

Customers can have account credits (negative balance) or debts (positive balance):

// Add $100 credit
customer.Balance = customer.Balance.Add(money.Money{
    Amount:   -10000, // Negative = credit
    Currency: "USD",
})

// Check balance before charging
if customer.Balance.Amount < 0 {
    // Apply credit to invoice
    creditApplied := min(invoice.Total.Amount, -customer.Balance.Amount)
    invoice.Total = invoice.Total.Add(money.Money{
        Amount:   -creditApplied,
        Currency: invoice.Total.Currency,
    })
    customer.Balance = customer.Balance.Add(money.Money{
        Amount:   creditApplied,
        Currency: customer.Balance.Currency,
    })
}

Tax IDs

Customers can provide tax IDs for VAT, GST, or other tax exemptions:

type TaxID struct {
    Type  string // "eu_vat", "us_ein", "au_abn", "in_gst"
    Value string
}

customer.TaxIDs = []customer.TaxID{
    {Type: "eu_vat", Value: "DE123456789"},
}

// Tax calculator uses this to determine exemptions
taxAmount, exempt := tax.Calculate(ctx, invoice, customer)

Customer portal

Generate a secure link to the customer billing portal:

portal, err := portal.CreateSession(ctx, customer.ID, &portal.SessionRequest{
    ReturnURL: "https://myapp.com/billing",
})

// Redirect customer to portal.URL
http.Redirect(w, r, portal.URL, http.StatusSeeOther)

The portal allows customers to:

  • View invoices and payment history
  • Update payment methods
  • Change subscription plans
  • Download receipts

Customer subscriptions

Get all subscriptions for a customer:

subscriptions, err := store.GetSubscriptionsByCustomer(ctx, customer.ID)

for _, sub := range subscriptions {
    plan, _ := store.GetPlan(ctx, sub.PlanID)
    fmt.Printf("Plan: %s, Status: %s\n", plan.Name, sub.Status)
}

Customer invoices

Retrieve billing history:

invoices, err := store.GetInvoicesByCustomer(ctx, customer.ID)

for _, inv := range invoices {
    fmt.Printf("%s: %s - %s\n", inv.Number, inv.Total, inv.Status)
}

Customer usage

View current period usage:

usage, err := meter.GetUsageByCustomer(ctx, customer.ID)

for meter, agg := range usage {
    fmt.Printf("%s: %.0f (limit: %d)\n", meter, agg.Sum, agg.Limit)
}

Customer lifecycle events

Ledger emits events for customer actions:

EventTrigger
customer.createdNew customer created
customer.updatedCustomer details changed
customer.deletedCustomer deleted
customer.payment_method.addedPayment method added
customer.payment_method.removedPayment method removed
customer.balance.updatedCredit/debit applied

Use webhooks or event handlers to sync with CRM systems:

events.Subscribe("customer.created", func(ctx context.Context, event *events.Event) {
    customer := event.Data.(*customer.Customer)
    crm.SyncCustomer(ctx, customer)
})

Deleting customers

Customers can only be deleted if they have no active subscriptions:

if err := store.DeleteCustomer(ctx, customer.ID); err != nil {
    if errors.Is(err, customer.ErrHasActiveSubscriptions) {
        return fmt.Errorf("cannot delete: customer has active subscriptions")
    }
    return err
}

To preserve billing history, use soft deletes:

customer.DeletedAt = &now
customer.Status = customer.StatusDeleted
store.UpdateCustomer(ctx, customer)

Multi-currency support

Customers are billed in their preferred currency:

customer := &customer.Customer{
    Currency: "EUR", // Customer pays in Euros
}

plan := &plan.Plan{
    Pricing: plan.Pricing{
        Amount: money.Money{
            Amount:   4999, // $49.99 USD
            Currency: "USD",
        },
    },
}

// Convert at invoice generation time
invoice.Total = currencyConverter.Convert(
    plan.Pricing.Amount,
    customer.Currency,
)
// Invoice.Total = money.Money{Amount: 4699, Currency: "EUR"} (€46.99)

Customer export

Export customer data for GDPR compliance:

export, err := customer.Export(ctx, customer.ID)

// export contains:
// - Customer profile
// - All subscriptions
// - All invoices
// - All payments
// - All usage events
// - All entitlement checks

json.NewEncoder(w).Encode(export)

Store interface

type Store interface {
    CreateCustomer(ctx context.Context, customer *Customer) error
    GetCustomer(ctx context.Context, customerID id.CustomerID) (*Customer, error)
    GetCustomerByEmail(ctx context.Context, email string) (*Customer, error)
    UpdateCustomer(ctx context.Context, customer *Customer) error
    DeleteCustomer(ctx context.Context, customerID id.CustomerID) error
    ListCustomers(ctx context.Context, filter *ListFilter) ([]*Customer, error)
    AddCredit(ctx context.Context, customerID id.CustomerID, amount money.Money, reason string) error
}

API routes

MethodPathDescription
POST/ledger/customersCreate a customer
GET/ledger/customersList customers
GET/ledger/customers/{id}Get customer by ID
GET/ledger/customers/email/{email}Get customer by email
PUT/ledger/customers/{id}Update customer
DELETE/ledger/customers/{id}Delete customer
POST/ledger/customers/{id}/creditAdd account credit
GET/ledger/customers/{id}/subscriptionsList customer subscriptions
GET/ledger/customers/{id}/invoicesList customer invoices
GET/ledger/customers/{id}/usageGet current usage
POST/ledger/customers/{id}/portalCreate portal session
GET/ledger/customers/{id}/exportExport customer data

On this page