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:
| Event | Trigger |
|---|---|
customer.created | New customer created |
customer.updated | Customer details changed |
customer.deleted | Customer deleted |
customer.payment_method.added | Payment method added |
customer.payment_method.removed | Payment method removed |
customer.balance.updated | Credit/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
| Method | Path | Description |
|---|---|---|
POST | /ledger/customers | Create a customer |
GET | /ledger/customers | List 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}/credit | Add account credit |
GET | /ledger/customers/{id}/subscriptions | List customer subscriptions |
GET | /ledger/customers/{id}/invoices | List customer invoices |
GET | /ledger/customers/{id}/usage | Get current usage |
POST | /ledger/customers/{id}/portal | Create portal session |
GET | /ledger/customers/{id}/export | Export customer data |