ledger

package
v1.40.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package ledger implements a double-entry accounting ledger for the billing engine.

Invariants:

  1. Every Entry has Postings that sum to exactly zero.
  2. Idempotency keys prevent duplicate entries within a tenant.
  3. Balances are derived exclusively from postings and holds.
  4. Holds track the authorize -> capture/void lifecycle.
  5. All operations are tenant-isolated.

Index

Constants

This section is empty.

Variables

View Source
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 NewMemLedger

func NewMemLedger() *MemLedger

NewMemLedger creates a new in-memory ledger.

func (*MemLedger) CaptureHold

func (m *MemLedger) CaptureHold(ctx context.Context, holdID string, amount int64) (*Entry, error)

func (*MemLedger) CreateAccount

func (m *MemLedger) CreateAccount(_ context.Context, a *Account) error

func (*MemLedger) CreateHold

func (m *MemLedger) CreateHold(_ context.Context, h *Hold) error

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 (m *MemLedger) GetAccount(_ context.Context, id string) (*Account, error)

func (*MemLedger) GetBalance

func (m *MemLedger) GetBalance(_ context.Context, accountID string, currency string) (*Balance, error)

func (*MemLedger) GetEntry

func (m *MemLedger) GetEntry(_ context.Context, id string) (*Entry, error)

func (*MemLedger) ListEntries

func (m *MemLedger) ListEntries(_ context.Context, f EntryFilter) ([]*Entry, error)

func (*MemLedger) PostEntry

func (m *MemLedger) PostEntry(_ context.Context, e *Entry) error

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)

func (*MemLedger) VoidHold

func (m *MemLedger) VoidHold(_ context.Context, holdID string) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL