Documentation
¶
Overview ¶
Package ledger implements a double-entry accounting ledger for the billing engine.
Invariants:
- Every Entry has Postings that sum to exactly zero.
- Idempotency keys prevent duplicate entries within a tenant.
- Balances are derived exclusively from postings and holds.
- Holds track the authorize -> capture/void lifecycle.
- All operations are tenant-isolated.
Index ¶
- Variables
- type Account
- type AccountType
- type Balance
- type Entry
- type EntryFilter
- type Hold
- type HoldStatus
- type Ledger
- type MemLedger
- func (m *MemLedger) CaptureHold(ctx context.Context, holdID string, amount int64) (*Entry, error)
- func (m *MemLedger) CreateAccount(_ context.Context, a *Account) error
- func (m *MemLedger) CreateHold(_ context.Context, h *Hold) error
- func (m *MemLedger) EnsureAccount(ctx context.Context, tenantID, name string, acctType AccountType, ...) (*Account, error)
- func (m *MemLedger) GetAccount(_ context.Context, id string) (*Account, error)
- func (m *MemLedger) GetBalance(_ context.Context, accountID string, currency string) (*Balance, error)
- func (m *MemLedger) GetEntry(_ context.Context, id string) (*Entry, error)
- func (m *MemLedger) ListEntries(_ context.Context, f EntryFilter) ([]*Entry, error)
- func (m *MemLedger) PostEntry(_ context.Context, e *Entry) error
- func (m *MemLedger) RecordDispute(ctx context.Context, tenantID, disputeID string, amount int64, cur string, ...) (*Entry, error)
- func (m *MemLedger) RecordPayment(ctx context.Context, tenantID, paymentIntentID string, amount int64, ...) (*Entry, error)
- func (m *MemLedger) RecordPayout(ctx context.Context, tenantID, payoutID string, amount int64, cur string, ...) (*Entry, error)
- func (m *MemLedger) RecordRefund(ctx context.Context, tenantID, refundID string, amount int64, cur string, ...) (*Entry, error)
- func (m *MemLedger) VoidHold(_ context.Context, holdID string) error
- type Posting
Constants ¶
This section is empty.
Variables ¶
var ( ErrPostingsNotBalanced = errors.New("ledger: postings do not sum to zero") ErrDuplicateEntry = errors.New("ledger: duplicate idempotency key") ErrAccountNotFound = errors.New("ledger: account not found") ErrEntryNotFound = errors.New("ledger: entry not found") ErrHoldNotFound = errors.New("ledger: hold not found") ErrHoldNotPending = errors.New("ledger: hold is not in pending status") ErrCaptureExceedsHold = errors.New("ledger: capture amount exceeds hold") ErrInsufficientBalance = errors.New("ledger: insufficient available balance") ErrInvalidAmount = errors.New("ledger: amount must be positive") ErrEmptyPostings = errors.New("ledger: entry must have at least two postings") )
Functions ¶
This section is empty.
Types ¶
type Account ¶
type Account struct {
ID string `json:"id"`
TenantID string `json:"tenantId"`
Name string `json:"name"`
Type AccountType `json:"type"`
Currency string `json:"currency"`
NormalBalance string `json:"normalBalance"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
Account represents a named account in the chart of accounts.
type AccountType ¶
type AccountType string
AccountType classifies an account in the chart of accounts.
const ( Asset AccountType = "asset" Liability AccountType = "liability" Equity AccountType = "equity" Revenue AccountType = "revenue" Expense AccountType = "expense" )
func (AccountType) NormalBalance ¶
func (t AccountType) NormalBalance() string
NormalBalance returns whether the natural balance of this account type increases with debits or credits.
type Balance ¶
type Balance struct {
AccountID string `json:"accountId"`
Currency string `json:"currency"`
PostedBalance int64 `json:"postedBalance"`
PendingBalance int64 `json:"pendingBalance"`
HeldBalance int64 `json:"heldBalance"`
AvailableBalance int64 `json:"availableBalance"` // posted - held
UpdatedAt time.Time `json:"updatedAt"`
}
Balance is the materialized balance for an account+currency pair.
type Entry ¶
type Entry struct {
ID string `json:"id"`
TenantID string `json:"tenantId"`
IdempotencyKey string `json:"idempotencyKey"`
Description string `json:"description"`
PaymentIntentID string `json:"paymentIntentId,omitempty"`
RefundID string `json:"refundId,omitempty"`
PayoutID string `json:"payoutId,omitempty"`
TransferID string `json:"transferId,omitempty"`
DisputeID string `json:"disputeId,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Postings []Posting `json:"postings"`
CreatedAt time.Time `json:"createdAt"`
}
Entry is a journal entry grouping one or more postings.
type EntryFilter ¶
type EntryFilter struct {
TenantID string
AccountID string
PaymentIntentID string
RefundID string
PayoutID string
DisputeID string
CreatedAfter time.Time
CreatedBefore time.Time
Limit int
}
EntryFilter controls listing of entries.
type Hold ¶
type Hold struct {
ID string `json:"id"`
TenantID string `json:"tenantId"`
AccountID string `json:"accountId"`
Amount int64 `json:"amount"` // always positive
Currency string `json:"currency"`
Status HoldStatus `json:"status"`
PaymentIntentID string `json:"paymentIntentId,omitempty"`
CapturedEntryID string `json:"capturedEntryId,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
Hold represents a pending authorization hold against an account.
type HoldStatus ¶
type HoldStatus string
HoldStatus represents the lifecycle state of an authorization hold.
const ( HoldPending HoldStatus = "pending" HoldCaptured HoldStatus = "captured" HoldVoided HoldStatus = "voided" HoldExpired HoldStatus = "expired" )
type Ledger ¶
type Ledger interface {
// Accounts
CreateAccount(ctx context.Context, account *Account) error
GetAccount(ctx context.Context, id string) (*Account, error)
// Entries (double-entry postings)
PostEntry(ctx context.Context, entry *Entry) error // validates sum=0, idempotency
GetEntry(ctx context.Context, id string) (*Entry, error)
ListEntries(ctx context.Context, filter EntryFilter) ([]*Entry, error)
// Holds (auth captures)
CreateHold(ctx context.Context, hold *Hold) error
CaptureHold(ctx context.Context, holdID string, amount int64) (*Entry, error)
VoidHold(ctx context.Context, holdID string) error
// Balances
GetBalance(ctx context.Context, accountID string, currency string) (*Balance, error)
// High-level operations
RecordPayment(ctx context.Context, tenantID, paymentIntentID string, amount int64, currency string, customerID string, fees int64) (*Entry, error)
RecordRefund(ctx context.Context, tenantID, refundID string, amount int64, currency string, customerID string) (*Entry, error)
RecordPayout(ctx context.Context, tenantID, payoutID string, amount int64, currency string, merchantID string) (*Entry, error)
RecordDispute(ctx context.Context, tenantID, disputeID string, amount int64, currency string, customerID string) (*Entry, error)
}
Ledger defines the double-entry ledger operations.
type MemLedger ¶
type MemLedger struct {
// contains filtered or unexported fields
}
MemLedger is an in-memory Ledger for testing and development.
func (*MemLedger) CaptureHold ¶
func (*MemLedger) CreateAccount ¶
func (*MemLedger) EnsureAccount ¶
func (m *MemLedger) EnsureAccount(ctx context.Context, tenantID, name string, acctType AccountType, currency string) (*Account, error)
EnsureAccount finds or creates a named account within a tenant.
func (*MemLedger) GetAccount ¶
func (*MemLedger) GetBalance ¶
func (*MemLedger) ListEntries ¶
func (*MemLedger) RecordDispute ¶
func (m *MemLedger) RecordDispute(ctx context.Context, tenantID, disputeID string, amount int64, cur string, customerID string) (*Entry, error)
RecordDispute creates a journal entry moving funds into a dispute hold account:
Debit platform:disputes_held (amount) Credit platform:cash (amount)
func (*MemLedger) RecordPayment ¶
func (m *MemLedger) RecordPayment(ctx context.Context, tenantID, paymentIntentID string, amount int64, cur string, customerID string, fees int64) (*Entry, error)
RecordPayment creates a journal entry for a successful payment:
Debit platform:cash (amount)
Credit customer_balance:{cust} (amount - fees)
Credit platform:fees (fees)
If fees == 0, the full amount credits the customer balance account.
func (*MemLedger) RecordPayout ¶
func (m *MemLedger) RecordPayout(ctx context.Context, tenantID, payoutID string, amount int64, cur string, merchantID string) (*Entry, error)
RecordPayout creates a journal entry for a merchant payout:
Debit merchant_settlement:{merchant} (amount)
Credit platform:cash (amount)
func (*MemLedger) RecordRefund ¶
func (m *MemLedger) RecordRefund(ctx context.Context, tenantID, refundID string, amount int64, cur string, customerID string) (*Entry, error)
RecordRefund creates a journal entry reversing a payment:
Debit customer_balance:{cust} (amount)
Credit platform:cash (amount)
type Posting ¶
type Posting struct {
ID string `json:"id"`
EntryID string `json:"entryId"`
AccountID string `json:"accountId"`
Amount int64 `json:"amount"` // positive=debit, negative=credit
Currency string `json:"currency"`
CreatedAt time.Time `json:"createdAt"`
}
Posting is a single debit or credit leg of a journal entry. Positive amount = debit, negative amount = credit.