ledger

package
v1.0.0-beta.232 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

Ledger

The ledger is OpenMeter's immutable accounting journal for customer credit, receivables, accrued usage, recognized earnings, payments, currency conversion, and breakage.

Domain model

Accounts express ownership and accounting purpose. Subaccounts are the actual posting addresses; a subaccount's normalized route is part of its identity.

Customer accounts are provisioned per customer. Business accounts are shared within a namespace.

account meaning
customer_fbo customer credit or stored value; not a literal regulated FBO bank account
customer_receivable value owed by the customer, including open and payment-authorized stages
customer_accrued acknowledged usage or spend not yet recognized as earnings
wash the external payment or cash boundary
earnings recognized business revenue
brokerage business-side counterparty for currency conversion
breakage expired or otherwise forfeited customer credit

The historical ledger stores immutable transaction groups, transactions, and entries. Balances are projections over those entries; they are not mutable facts stored independently from the journal.

Ownership and boundaries

  • The ledger owns balanced posting, route identity, permitted accounting flows, collection from concrete balances, corrections, and accounting provenance.
  • Transaction templates encode posting mechanics. They do not decide charge lifecycle, payment lifecycle, settlement mode, or invoice orchestration.
  • chargeadapter translates charge lifecycle events into ledger templates. Charges decide when an effect is due; the ledger decides how that effect is represented and validated. See the charges domain.
  • The collector owns source selection and correction unwind order. Callers provide the target amount and attribution; they must not recreate collection policy.
  • customerbalance is a customer-facing projection over booked ledger state, breakage, and not-yet-booked charge impacts. It does not own posting, collection, or correction rules.
  • Account resolvers provision canonical customer and namespace business accounts. Higher-level domains request account-specific route parameters rather than assembling generic accounts or posting addresses.

Transaction invariants

  • Every transaction sums to zero. Each entry amount is valid at the posting currency's precision.
  • CommitGroup validates the whole input, locks every affected parent account, and books the group atomically in the caller's database transaction.
  • Default routing rules constrain allowed account-type combinations, flow direction, authorization stages, route compatibility, and dimension scope. A balanced transaction can still be invalid.
  • Reversal and correction logic follows the actual original entries. It preserves charge provenance and route pairing, links replacement postings to their source entries, and uses deterministic source order rather than recomputing an idealized replacement from current balances.
  • Entry identity records collection or correction linkage and source and spend charge provenance when present. Template codes and transaction annotations describe accounting meaning.

The historical ledger makes a group atomic, but it does not deduplicate a repeated CommitGroup call. The initiating domain must make retries safe and persist the returned group reference with its own lifecycle state. Ledger annotations and entry identity preserve accounting meaning and provenance; they are not operation idempotency keys.

Route invariants

Routes currently carry currency, feature restrictions, cost basis, credit priority, receivable authorization status, tax code, and tax behavior. These are accounting identity, not optional metadata. Dropping a populated dimension during translation or filtering can merge economically distinct balances while leaving each transaction locally balanced.

  • Routes are normalized before key creation and querying. Feature order is not semantic.
  • Route.Filter() pins present values, including explicit nil values. RouteFilter absence means "do not filter"; it differs from filtering for a nil route dimension.
  • Feature dimensions belong only on FBO and receivable routes. Tax dimensions belong only on accrued and earnings routes; FBO sources acquire the charge's tax attribution when value moves into accrued.
  • Currency and cost-basis attribution survive the relevant FBO, receivable, accrued, earnings, wash, brokerage, and breakage legs.
  • Credit priority controls FBO collection order. Corrections unwind the recorded collection order rather than applying today's priorities.
  • Receivable authorization is an accounting stage. Moving between open and authorized routes is a ledger transaction, not an in-place flag update.

Adding or changing a route dimension is a storage and compatibility change. It affects normalization, routing-key versions, schema persistence, filters, account-specific route parameters, transaction rules, corrections, and historical data—not only the Route struct.

Feature-restricted balances

Public balance filtering and source allocability are related but distinct:

  • no feature filter means the whole credit portfolio
  • an unrestricted-only filter selects routes with no feature restriction
  • filtering for one feature includes unrestricted routes and routes containing that feature
  • unrestricted credit can fund any charge; restricted credit can fund only a matching feature

Public filtering cannot be implemented as exact route equality, and a public balance result is not necessarily the set of sources allocable to every charge.

Time and balance boundaries

  • BookedAt is accounting effective time. CreatedAt and transaction ID make ordering deterministic when booked times tie.
  • AsOf and cursor behavior apply before customer-facing projection; future ledger or breakage facts cannot leak into historical views.
  • historical.Ledger enforces transaction invariance, not post-transaction account-balance constraints. Product-specific bounds, including when a balance may go negative, belong to higher-level flows and collectors.

Documentation

Index

Constants

View Source
const (
	AnnotationChargeNamespace     = "ledger.charge.namespace"
	AnnotationChargeID            = "ledger.charge.id"
	AnnotationSubscriptionID      = "ledger.subscription.id"
	AnnotationSubscriptionPhaseID = "ledger.subscription.phase.id"
	AnnotationSubscriptionItemID  = "ledger.subscription.item.id"
	AnnotationFeatureID           = "ledger.feature.id"

	AnnotationTransactionTemplateCode = "ledger.transaction.template_code"
	AnnotationTransactionDirection    = "ledger.transaction.direction"
	AnnotationCollectionType          = "ledger.collection.type"
	AnnotationCollectionSourceOrder   = "ledger.collection.source_order"
	AnnotationBreakageKind            = "ledger.breakage.kind"
	AnnotationBreakageRecordID        = "ledger.breakage.record_id"
	AnnotationBreakagePlanID          = "ledger.breakage.plan_id"
)
View Source
const (
	BalanceBucketGroupBySourceChargeID = "source_charge_id"
	BalanceBucketGroupBySpendChargeID  = "spend_charge_id"
)
View Source
const CollectionTypeBreakage = "breakage"
View Source
const DefaultCustomerFBOPriority = 100
View Source
const ErrCodeAddressInvalid models.ErrorCode = "ledger_address_invalid"
View Source
const ErrCodeBusinessAccountMissing models.ErrorCode = "ledger_business_account_missing"
View Source
const ErrCodeCostBasisInvalid models.ErrorCode = "ledger_cost_basis_invalid"
View Source
const ErrCodeCreditPriorityInvalid models.ErrorCode = "ledger_credit_priority_invalid"
View Source
const ErrCodeCurrencyInvalid models.ErrorCode = "ledger_currency_invalid"
View Source
const ErrCodeCustomerAccountMissing models.ErrorCode = "ledger_customer_account_missing"
View Source
const ErrCodeEntryInvalid models.ErrorCode = "ledger_entry_invalid"
View Source
const ErrCodeInvalidTransactionTotal models.ErrorCode = "invalid_transaction_total"
View Source
const ErrCodeListTransactionsInputInvalid models.ErrorCode = "ledger_list_transactions_input_invalid"
View Source
const ErrCodeResolutionScopeInvalid models.ErrorCode = "ledger_resolution_scope_invalid"
View Source
const ErrCodeResolutionTemplateUnknown models.ErrorCode = "ledger_resolution_template_unknown"
View Source
const ErrCodeRoutingKeyVersionInvalid models.ErrorCode = "ledger_routing_key_version_invalid"
View Source
const ErrCodeRoutingKeyVersionUnsupported models.ErrorCode = "ledger_routing_key_version_unsupported"
View Source
const ErrCodeRoutingRuleViolated models.ErrorCode = "ledger_routing_rule_violated"
View Source
const ErrCodeTaxBehaviorInvalid models.ErrorCode = "ledger_tax_behavior_invalid"
View Source
const ErrCodeTransactionAmountInvalid models.ErrorCode = "ledger_transaction_amount_invalid"
View Source
const ErrCodeTransactionAuthorizationStatusInvalid models.ErrorCode = "ledger_transaction_authorization_status_invalid"
View Source
const ErrCodeTransactionGroupEmpty models.ErrorCode = "ledger_transaction_group_empty"
View Source
const ErrCodeTransactionInputRequired models.ErrorCode = "ledger_transaction_input_required"

Variables

View Source
var ErrAddressInvalid = models.NewValidationIssue(
	ErrCodeAddressInvalid,
	"ledger posting address is invalid",
)
View Source
var ErrBusinessAccountMissing = models.NewValidationIssue(
	ErrCodeBusinessAccountMissing,
	"required business ledger account is missing",
)
View Source
var ErrCodeLedgerQueryInvalid models.ErrorCode = "ledger_query_invalid"
View Source
var ErrCostBasisInvalid = models.NewValidationIssue(
	ErrCodeCostBasisInvalid,
	"ledger cost basis is invalid",
)
View Source
var ErrCreditPriorityInvalid = models.NewValidationIssue(
	ErrCodeCreditPriorityInvalid,
	"ledger credit priority is invalid",
)
View Source
var ErrCurrencyInvalid = models.NewValidationIssue(
	ErrCodeCurrencyInvalid,
	"ledger currency is invalid",
)
View Source
var ErrCustomerAccountMissing = models.NewValidationIssue(
	ErrCodeCustomerAccountMissing,
	"required customer ledger account is missing",
)
View Source
var ErrEntryInvalid = models.NewValidationIssue(
	ErrCodeEntryInvalid,
	"ledger entry is invalid",
)
View Source
var ErrInvalidTransactionTotal = models.NewValidationIssue(
	ErrCodeInvalidTransactionTotal,
	"transaction total is invalid, credits and debits must sum to 0",
)
View Source
var ErrLedgerQueryInvalid = models.NewValidationIssue(
	ErrCodeLedgerQueryInvalid,
	"ledger query is invalid",
)
View Source
var ErrListTransactionsInputInvalid = models.NewValidationIssue(
	ErrCodeListTransactionsInputInvalid,
	"ledger list transactions input is invalid",
)
View Source
var ErrResolutionScopeInvalid = models.NewValidationIssue(
	ErrCodeResolutionScopeInvalid,
	"ledger resolution scope is invalid",
)
View Source
var ErrResolutionTemplateUnknown = models.NewValidationIssue(
	ErrCodeResolutionTemplateUnknown,
	"ledger transaction template type is unknown",
)
View Source
var ErrRoutingKeyVersionInvalid = models.NewValidationIssue(
	ErrCodeRoutingKeyVersionInvalid,
	"ledger routing key version is invalid",
)
View Source
var ErrRoutingKeyVersionUnsupported = models.NewValidationIssue(
	ErrCodeRoutingKeyVersionUnsupported,
	"ledger routing key version is unsupported",
)
View Source
var ErrRoutingRuleViolated = models.NewValidationIssue(
	ErrCodeRoutingRuleViolated,
	"ledger routing rule violated",
)
View Source
var ErrTaxBehaviorInvalid = models.NewValidationIssue(
	ErrCodeTaxBehaviorInvalid,
	"ledger tax behavior is invalid",
)
View Source
var ErrTransactionAmountInvalid = models.NewValidationIssue(
	ErrCodeTransactionAmountInvalid,
	"ledger transaction amount is invalid",
)
View Source
var ErrTransactionAuthorizationStatusInvalid = models.NewValidationIssue(
	ErrCodeTransactionAuthorizationStatusInvalid,
	"ledger transaction authorization status is invalid",
)
View Source
var ErrTransactionGroupEmpty = models.NewValidationIssue(
	ErrCodeTransactionGroupEmpty,
	"ledger transaction group must contain at least one transaction",
)
View Source
var ErrTransactionInputRequired = models.NewValidationIssue(
	ErrCodeTransactionInputRequired,
	"transaction input is required",
)

Functions

func BreakageAnnotations

func BreakageAnnotations(kind BreakageKind, recordID string, planID *string) models.Annotations

func ChargeAnnotations

func ChargeAnnotations(chargeID models.NamespacedID) models.Annotations

func EntryMatchesImpactFilter

func EntryMatchesImpactFilter(entry Entry, filter ImpactFilter) bool

func NewCollectionSourceIdentityKey

func NewCollectionSourceIdentityKey(index int) string

func NewCorrectionSourceIdentityKey

func NewCorrectionSourceIdentityKey(sourceEntryID string) string

func SortedFeatures

func SortedFeatures(features []string) []string

SortedFeatures returns a sorted copy of features for canonical storage. Returns nil if empty.

func TransactionAnnotations

func TransactionAnnotations(templateCode string, direction TransactionDirection) models.Annotations

func TransactionImpact

func TransactionImpact(tx Transaction, filter ImpactFilter) alpacadecimal.Decimal

func TransactionTemplateCodeFromAnnotations

func TransactionTemplateCodeFromAnnotations(annotations models.Annotations) (string, error)

func ValidateAddress

func ValidateAddress(ctx context.Context, address PostingAddress) error

func ValidateCostBasis

func ValidateCostBasis(value alpacadecimal.Decimal) error

func ValidateCostBasisCurrency

func ValidateCostBasisCurrency(currency currencyx.Code, costBasisCurrency *currencyx.Code, costBasis *alpacadecimal.Decimal) error

ValidateCostBasisCurrency validates the cost_basis_currency route dimension, which is the fiat currency a custom currency's cost basis is denominated in (also referred to as the route's cost_basis_currency).

costBasis is the route's cost_basis dimension. A custom-currency route with a known cost basis must also carry the fiat currency that cost basis is denominated in — otherwise USD-backed and EUR-backed custom balances with the same numeric cost basis could be routed onto the same sub-account.

func ValidateCreditPriority

func ValidateCreditPriority(value int) error

ValidateCreditPriority validates a credit priority integer value.

func ValidateCurrency

func ValidateCurrency(value currencyx.Code) error

ValidateCurrency validates a currency value.

func ValidateEntryIdentityKey

func ValidateEntryIdentityKey(entry EntryInput) error

func ValidateEntryInput

func ValidateEntryInput(ctx context.Context, entry EntryInput) error

func ValidateFeatures

func ValidateFeatures(features []string) error

ValidateFeatures validates feature route keys.

func ValidateInvariance

func ValidateInvariance(ctx context.Context, entries []EntryInput) error

ValidateInvariance validates that Debit - Credit = 0 for each currency in the given entries.

func ValidateRouting

func ValidateRouting(ctx context.Context, entries []EntryInput) error

func ValidateTransactionAmount

func ValidateTransactionAmount(value alpacadecimal.Decimal) error

func ValidateTransactionInput

func ValidateTransactionInput(ctx context.Context, transaction TransactionInput) error

func ValidateTransactionInputWith

func ValidateTransactionInputWith(ctx context.Context, transaction TransactionInput, routingValidator RoutingValidator) error

Types

type Account

type Account interface {
	ID() models.NamespacedID
	Type() AccountType
}

Account represents a ledger account tying together multiple sub-accounts. Accounts describe ownership and purpose while SubAccounts parameterize the actual posting address.

type AccountCatalog

type AccountCatalog interface {
	AccountReader
	AccountProvisioner
}

type AccountLocker

type AccountLocker interface {
	LockAccountsForPosting(ctx context.Context, accounts []Account) error
}

type AccountProvisioner

type AccountProvisioner interface {
	CreateAccount(ctx context.Context, input CreateAccountInput) (Account, error)
	EnsureSubAccount(ctx context.Context, input CreateSubAccountInput) (SubAccount, error)
}

type AccountReader

type AccountReader interface {
	GetAccountByID(ctx context.Context, id models.NamespacedID) (Account, error)
	GetSubAccountByID(ctx context.Context, id models.NamespacedID) (SubAccount, error)

	ListSubAccounts(ctx context.Context, input ListSubAccountsInput) ([]SubAccount, error)
	ListAccounts(ctx context.Context, input ListAccountsInput) ([]Account, error)
}

type AccountResolver

type AccountResolver interface {
	GetCustomerAccounts(ctx context.Context, customerID customer.CustomerID) (CustomerAccounts, error)
	EnsureBusinessAccounts(ctx context.Context, namespace string) (BusinessAccounts, error)
	GetBusinessAccounts(ctx context.Context, namespace string) (BusinessAccounts, error)
}

type AccountType

type AccountType string
const (
	AccountTypeCustomerFBO        AccountType = "customer_fbo" // is this the right name?
	AccountTypeCustomerReceivable AccountType = "customer_receivable"
	AccountTypeCustomerAccrued    AccountType = "customer_accrued"
)

Customer Accounts

const (
	AccountTypeWash      AccountType = "wash"
	AccountTypeEarnings  AccountType = "earnings"
	AccountTypeBrokerage AccountType = "brokerage"
	AccountTypeBreakage  AccountType = "breakage"
)

Shared Business Accounts

func (AccountType) Validate

func (t AccountType) Validate() error

type Balance

type Balance = alpacadecimal.Decimal

type BalanceBucket

type BalanceBucket struct {
	Address       PostingAddress
	GroupByValues map[string]*string
	SettledAmount alpacadecimal.Decimal
	PendingAmount alpacadecimal.Decimal
}

type BalanceBucketQuery

type BalanceBucketQuery struct {
	Namespace string
	Filters   Filters
	GroupBy   []string
}

func (BalanceBucketQuery) Validate

func (q BalanceBucketQuery) Validate() error

type BalanceQuerier

type BalanceQuerier interface {
	GetAccountBalance(ctx context.Context, account Account, route RouteFilter, query BalanceQuery) (Balance, error)
	GetSubAccountBalance(ctx context.Context, subAccount SubAccount, query BalanceQuery) (Balance, error)
	GetBalanceBuckets(ctx context.Context, query BalanceBucketQuery) ([]BalanceBucket, error)
}

type BalanceQuery

type BalanceQuery struct {
	After *TransactionCursor
	AsOf  *time.Time
}

type BreakageKind

type BreakageKind string
const (
	BreakageKindPlan    BreakageKind = "plan"
	BreakageKindRelease BreakageKind = "release"
	BreakageKindReopen  BreakageKind = "reopen"
)

func (BreakageKind) Values

func (BreakageKind) Values() []string

type BreakageSourceKind

type BreakageSourceKind string
const (
	BreakageSourceKindCreditPurchase           BreakageSourceKind = "credit_purchase"
	BreakageSourceKindUsage                    BreakageSourceKind = "usage"
	BreakageSourceKindUsageCorrection          BreakageSourceKind = "usage_correction"
	BreakageSourceKindCreditPurchaseCorrection BreakageSourceKind = "credit_purchase_correction"
	BreakageSourceKindAdvanceBackfill          BreakageSourceKind = "advance_backfill"
)

func (BreakageSourceKind) Values

func (BreakageSourceKind) Values() []string

type BusinessAccount

type BusinessAccount interface {
	Account

	GetSubAccountForRoute(ctx context.Context, route BusinessRouteParams) (SubAccount, error)
}

BusinessAccount is a business account.

type BusinessAccounts

type BusinessAccounts struct {
	WashAccount      BusinessAccount
	EarningsAccount  BusinessAccount
	BrokerageAccount BusinessAccount
	BreakageAccount  BusinessAccount
}

type BusinessRouteParams

type BusinessRouteParams struct {
	Currency          currencies.CurrencyReference
	CostBasisCurrency *currencyx.Code
	TaxCode           *string
	TaxBehavior       *TaxBehavior
	CostBasis         *alpacadecimal.Decimal
}

func (BusinessRouteParams) Route

func (p BusinessRouteParams) Route() Route

func (BusinessRouteParams) Validate

func (p BusinessRouteParams) Validate() error

type ChargeTransactionAnnotationsInput

type ChargeTransactionAnnotationsInput struct {
	ChargeID models.NamespacedID

	SubscriptionID      *string
	SubscriptionPhaseID *string
	SubscriptionItemID  *string
	FeatureID           *string
}

type CreateAccountInput

type CreateAccountInput struct {
	Namespace   string
	Type        AccountType
	Annotations models.Annotations
}

func (CreateAccountInput) Validate

func (c CreateAccountInput) Validate() error

type CreateSubAccountInput

type CreateSubAccountInput struct {
	Namespace   string
	AccountID   string
	Annotations models.Annotations
	Route       Route
}

func (CreateSubAccountInput) Validate

func (c CreateSubAccountInput) Validate() error

type CustomerAccount

type CustomerAccount interface {
	Account
}

CustomerAccount is a Customer specific account

type CustomerAccounts

type CustomerAccounts struct {
	FBOAccount        CustomerFBOAccount
	ReceivableAccount CustomerReceivableAccount
	AccruedAccount    CustomerAccruedAccount
}

type CustomerAccruedAccount

type CustomerAccruedAccount interface {
	CustomerAccount

	GetSubAccountForRoute(ctx context.Context, route CustomerAccruedRouteParams) (SubAccount, error)
}

CustomerAccruedAccount is a customer accrued account used as a staging area for usage that has been acknowledged but not yet recognized as earnings.

type CustomerAccruedRouteParams

type CustomerAccruedRouteParams struct {
	Currency          currencies.CurrencyReference
	CostBasisCurrency *currencyx.Code
	TaxCode           *string
	TaxBehavior       *TaxBehavior
	CostBasis         *alpacadecimal.Decimal
}

CustomerAccruedRouteParams are routing parameters specific to customer accrued sub-accounts.

func (CustomerAccruedRouteParams) Route

func (CustomerAccruedRouteParams) Validate

func (p CustomerAccruedRouteParams) Validate() error

type CustomerFBOAccount

type CustomerFBOAccount interface {
	CustomerAccount

	GetSubAccountForRoute(ctx context.Context, route CustomerFBORouteParams) (SubAccount, error)
}

CustomerFBOAccount is a customer FBO account.

type CustomerFBORouteParams

type CustomerFBORouteParams struct {
	Currency          currencies.CurrencyReference
	CostBasisCurrency *currencyx.Code
	CreditPriority    int
	Features          []string
	CostBasis         *alpacadecimal.Decimal
}

CustomerFBORouteParams are routing parameters specific to customer FBO sub-accounts. CreditPriority is required (non-pointer) — the type system enforces its presence.

func (CustomerFBORouteParams) Route

func (p CustomerFBORouteParams) Route() Route

func (CustomerFBORouteParams) Validate

func (p CustomerFBORouteParams) Validate() error

type CustomerReceivableAccount

type CustomerReceivableAccount interface {
	CustomerAccount

	GetSubAccountForRoute(ctx context.Context, route CustomerReceivableRouteParams) (SubAccount, error)
}

CustomerReceivableAccount is a customer receivable account.

type CustomerReceivableRouteParams

type CustomerReceivableRouteParams struct {
	Currency                       currencies.CurrencyReference
	CostBasisCurrency              *currencyx.Code
	TaxCode                        *string
	Features                       []string
	CostBasis                      *alpacadecimal.Decimal
	TransactionAuthorizationStatus TransactionAuthorizationStatus
}

CustomerReceivableRouteParams are routing parameters specific to customer receivable sub-accounts. TransactionAuthorizationStatus is required; callers must explicitly select the open or authorized route.

func (CustomerReceivableRouteParams) Route

func (CustomerReceivableRouteParams) Validate

func (p CustomerReceivableRouteParams) Validate() error

type Entry

type Entry interface {
	EntryInput
	ID() models.NamespacedID
	TransactionID() models.NamespacedID
}

type EntryIdentityKeyText

type EntryIdentityKeyText string

EntryIdentityKeyText is the string serialized representation of the identity key. This encodes all information of the key.

func (EntryIdentityKeyText) Parse

func (EntryIdentityKeyText) Version

type EntryIdentityParts

type EntryIdentityParts struct {
	CollectionSource *string // Custom key to keep 1:1 matching logic during collection
	CorrectionSource *string // References the ID of the entry we're collecting
	SourceChargeID   *string // The original creditpurchase charge (if exists) that funds this entry
	SpendChargeID    *string // The usage charge (if exists) that accrued this entry
}

func (EntryIdentityParts) Text

type EntryIdentityVersion

type EntryIdentityVersion int
const (
	// legacy
	EntryIdentityVersion1 EntryIdentityVersion = 1
	// charge provenance
	EntryIdentityVersion2 EntryIdentityVersion = 2
)

type EntryInput

type EntryInput interface {
	PostingAddress() PostingAddress
	Amount() alpacadecimal.Decimal
	IdentityKey() string
	SchemaVersion() EntrySchemaVersion
	SourceChargeID() *string
	SpendChargeID() *string
	Annotations() models.Annotations
}

type EntrySchemaVersion

type EntrySchemaVersion int
const (
	EntrySchemaVersionLegacy  EntrySchemaVersion = 1
	EntrySchemaVersionCurrent EntrySchemaVersion = 2
)

type Filters

type Filters struct {
	// BookedAtPeriod is inclusive-exclusive... should it be? Maybe finally add period inclusivity params?
	BookedAtPeriod *timeutil.OpenPeriod
	After          *TransactionCursor
	AsOf           *time.Time
	TransactionID  *string
	// AccountID narrows the query to a single account via its sub-accounts.
	AccountID *string
	// SourceChargeID narrows entries by the creditpurchase charge that funded them.
	// Absent aggregates across source charges, Some(nil) selects source-less entries.
	SourceChargeID mo.Option[*string]
	// SpendChargeID narrows entries by the charge that consumed or accrued value.
	// Absent aggregates across spend charges, Some(nil) selects spend-less entries.
	SpendChargeID mo.Option[*string]
	// Route is a partial route filter. AccountID plus a full Route.Filter() identifies
	// one sub-account, but arbitrary RouteFilter values can match many sub-accounts.
	Route RouteFilter
}

type ImpactFilter

type ImpactFilter struct {
	AccountType AccountType
	Route       RouteFilter
}

type Ledger

type Ledger interface {
	// CommitGroup commits a list of transactions on the Ledger atomically
	CommitGroup(ctx context.Context, group TransactionGroupInput) (TransactionGroup, error)

	// GetTransactionGroup loads a previously committed transaction group including its transactions.
	GetTransactionGroup(ctx context.Context, id models.NamespacedID) (TransactionGroup, error)

	// ListTransactions lists transactions on the Ledger according to some filters
	//
	// TODO: Cursoring gets problematic due to diff between wall_clock and booked_at. It would be convenient to return in order of booked_at as that simplifies parsing. This API will likely change.
	ListTransactions(ctx context.Context, params ListTransactionsInput) (ListTransactionsResult, error)
}

type ListAccountsInput

type ListAccountsInput struct {
	Namespace    string
	AccountTypes []AccountType
}

type ListSubAccountsInput

type ListSubAccountsInput struct {
	Namespace string
	AccountID string

	Route RouteFilter
}

type ListTransactionsCreditMovement

type ListTransactionsCreditMovement uint8
const (
	ListTransactionsCreditMovementUnspecified ListTransactionsCreditMovement = iota
	ListTransactionsCreditMovementPositive
	ListTransactionsCreditMovementNegative
)

type ListTransactionsInput

type ListTransactionsInput struct {
	Namespace string
	Cursor    *TransactionCursor
	Before    *TransactionCursor
	Limit     int

	TransactionID *models.NamespacedID

	// AccountIDs scopes the query to transactions with entries on these accounts.
	AccountIDs []string
	Currency   *currencyx.Code
	AsOf       *time.Time
	Route      RouteFilter

	CreditMovement ListTransactionsCreditMovement

	// AnnotationFilters matches transactions whose annotations contain all the given key-value pairs.
	AnnotationFilters map[string]string

	// ExcludeAnnotationFilters excludes transactions whose annotations contain any given key-value pair.
	ExcludeAnnotationFilters map[string]string
}

func (ListTransactionsInput) Validate

func (i ListTransactionsInput) Validate() error

type ListTransactionsResult

type ListTransactionsResult struct {
	Items      []Transaction
	NextCursor *TransactionCursor
}

type PostingAddress

type PostingAddress interface {
	models.Equaler[PostingAddress]

	SubAccountID() string
	AccountType() AccountType
	Route() SubAccountRoute
}

PostingAddress encapsulates an address you can post-against. This is a one-to-one mapping to a SubAccount, this address format exists for routing purposes where the full sub-sccount isn't needed.

type Query

type Query struct {
	Namespace string

	Cursor *pagination.Cursor

	Filters Filters
}

func (Query) Validate

func (p Query) Validate() error

type Route

type Route struct {
	// Version is auto-derived by Normalize() based on which fields are present.
	// Direct assignment is overwritten on the next Normalize call. Do not set
	// manually except for testing edge cases.
	Version  RoutingKeyVersion
	Currency currencies.CurrencyReference
	// CostBasisCurrency identifies the fiat currency exchanged into a custom currency.
	// Fiat and non-exchange routes keep CostBasisCurrency nil.
	CostBasisCurrency *currencyx.Code
	TaxCode           *string
	// TaxBehavior distinguishes taxable accrued and earnings buckets.
	// Customer FBO routes do not carry tax dimensions; credit sources are
	// attributed to charge tax configuration when they accrue.
	TaxBehavior                    *TaxBehavior
	Features                       []string
	CostBasis                      *alpacadecimal.Decimal
	CreditPriority                 *int
	TransactionAuthorizationStatus *TransactionAuthorizationStatus
}

Route holds the literal values that identify a sub-account's routing path. It is used for creation, persistence, and routing key generation.

func (Route) Filter

func (r Route) Filter() RouteFilter

Filter converts a Route to a RouteFilter for use in queries. All present route fields are pinned as exact-match filters (including nil values).

func (Route) Matches

func (r Route) Matches(filter RouteFilter) bool

func (Route) Normalize

func (r Route) Normalize() (Route, error)

Normalize canonicalizes route values so semantically equivalent routes share the same stored literals and routing keys. It also sets Version based on the fields present in the route.

func (Route) Validate

func (r Route) Validate() error

Validate validates a route intended for creation and persistence. Custom currency routes must carry a hydrated immutable definition snapshot.

type RouteFilter

type RouteFilter struct {
	Currency          currencies.CurrencyReference
	CostBasisCurrency mo.Option[*currencyx.Code]

	// Non-currency fields are retained for near-future expansion.
	TaxCode     mo.Option[*string]
	TaxBehavior mo.Option[*TaxBehavior]
	// Features is an exact-match filter for sub-account routes.
	Features mo.Option[[]string]
	// MatchFeature matches unrestricted routes and routes containing this single feature key.
	MatchFeature string
	CostBasis    mo.Option[*alpacadecimal.Decimal]

	// CreditPriority is only meaningful for customer_fbo queries.
	CreditPriority *int

	// TransactionAuthorizationStatus is currently only meaningful for customer_receivable queries.
	// Nil means "do not filter by authorization status", not "open".
	TransactionAuthorizationStatus *TransactionAuthorizationStatus
}

RouteFilter is the set of route fields that can be used to filter sub-accounts and query balances.

func (RouteFilter) Normalize

func (f RouteFilter) Normalize() (RouteFilter, error)

Normalize canonicalizes route filter values before querying.

type RoutingKey

type RoutingKey struct {
	// contains filtered or unexported fields
}

func BuildRoutingKey

func BuildRoutingKey(route Route) (RoutingKey, error)

BuildRoutingKey normalizes route and encodes it as a RoutingKey. The version is determined automatically from the route fields present.

func BuildRoutingKeyV1

func BuildRoutingKeyV1(route Route) (RoutingKey, error)

BuildRoutingKeyV1 encodes route as a V1 routing key. Returns an error if route.TaxBehavior is non-nil; use BuildRoutingKey to select the correct version automatically based on route fields.

func BuildRoutingKeyV2

func BuildRoutingKeyV2(route Route) (RoutingKey, error)

BuildRoutingKeyV2 encodes route as a V2 routing key.

func BuildRoutingKeyV3

func BuildRoutingKeyV3(route Route) (RoutingKey, error)

BuildRoutingKeyV3 encodes route as a V3 routing key.

func BuildRoutingKeyV4

func BuildRoutingKeyV4(route Route) (RoutingKey, error)

BuildRoutingKeyV4 encodes route as a V4 routing key.

func NewRoutingKey

func NewRoutingKey(version RoutingKeyVersion, value string) (RoutingKey, error)

func (RoutingKey) Value

func (k RoutingKey) Value() string

func (RoutingKey) Version

func (k RoutingKey) Version() RoutingKeyVersion

type RoutingKeyVersion

type RoutingKeyVersion string
const (
	RoutingKeyVersionV1 RoutingKeyVersion = "v1"
	// RoutingKeyVersionV2 extends V1 by adding the tax_behavior segment.
	// Use V2 when a route has a non-nil TaxBehavior; otherwise use V1 for
	// backward compatibility with sub-accounts created before tax_behavior existed.
	RoutingKeyVersionV2 RoutingKeyVersion = "v2"
	// RoutingKeyVersionV3 extends V2 by adding the cost_basis_currency segment.
	RoutingKeyVersionV3 RoutingKeyVersion = "v3"
	// RoutingKeyVersionV4 extends V3 by storing the complete custom currency
	// reference snapshot in the currency segment.
	RoutingKeyVersionV4 RoutingKeyVersion = "v4"
)

func (RoutingKeyVersion) Validate

func (v RoutingKeyVersion) Validate() error

type RoutingValidator

type RoutingValidator interface {
	ValidateEntries(entries []EntryInput) error
}

type SubAccount

type SubAccount interface {
	// Returns the address of the sub-account
	Address() PostingAddress

	// AccountID returns the identifier of the parent account.
	AccountID() models.NamespacedID

	// Route returns the routing values of the sub-account.
	Route() Route
}

SubAccount is an actual address you can post against. It has all required routing information provided. Accounts describe ownership and purpose while SubAccounts parameterize the actual posting address.

type SubAccountRoute

type SubAccountRoute struct {
	// contains filtered or unexported fields
}

func NewSubAccountRouteFromData

func NewSubAccountRouteFromData(data SubAccountRouteData) (SubAccountRoute, error)

NewSubAccountRouteFromData hydrates a sub-account route from persisted data. Does not enforce Route & RoutingKey equality due to possible version mismatch. Sets route.Version from the stored routing key so the round-trip is transparent.

func (SubAccountRoute) ID

func (r SubAccountRoute) ID() string

func (SubAccountRoute) Route

func (r SubAccountRoute) Route() Route

func (SubAccountRoute) RoutingKey

func (r SubAccountRoute) RoutingKey() RoutingKey

type SubAccountRouteData

type SubAccountRouteData struct {
	ID         string
	RoutingKey RoutingKey
	Route      Route
}

type TaxBehavior

type TaxBehavior string
const (
	TaxBehaviorInclusive TaxBehavior = "inclusive"
	TaxBehaviorExclusive TaxBehavior = "exclusive"
)

func (TaxBehavior) Validate

func (b TaxBehavior) Validate() error

type Transaction

type Transaction interface {
	Cursor() TransactionCursor
	BookedAt() time.Time
	Entries() []Entry
	ID() models.NamespacedID
	Annotations() models.Annotations
}

Transaction represents a list of entries booked at the same time

type TransactionAuthorizationStatus

type TransactionAuthorizationStatus string
const (
	TransactionAuthorizationStatusOpen       TransactionAuthorizationStatus = "open"
	TransactionAuthorizationStatusAuthorized TransactionAuthorizationStatus = "authorized"
)

func (TransactionAuthorizationStatus) Validate

type TransactionCursor

type TransactionCursor struct {
	BookedAt  time.Time
	CreatedAt time.Time
	ID        models.NamespacedID
}

func (TransactionCursor) Compare

func (c TransactionCursor) Compare(other TransactionCursor) int

Compare returns cursor ordering by BookedAt, then CreatedAt, then ID. It returns -1 if c < other, 0 if equal, and 1 if c > other.

func (TransactionCursor) Validate

func (c TransactionCursor) Validate() error

type TransactionDirection

type TransactionDirection string
const (
	TransactionDirectionForward    TransactionDirection = "forward"
	TransactionDirectionCorrection TransactionDirection = "correction"
)

func TransactionDirectionFromAnnotations

func TransactionDirectionFromAnnotations(annotations models.Annotations) (TransactionDirection, error)

type TransactionGroup

type TransactionGroup interface {
	ID() models.NamespacedID
	Transactions() []Transaction
	Annotations() models.Annotations
}

TransactionGroup represents a group of transactions written to the ledger at the same time

type TransactionGroupInput

type TransactionGroupInput interface {
	Namespace() string
	Transactions() []TransactionInput
	Annotations() models.Annotations
}

type TransactionInput

type TransactionInput interface {
	BookedAt() time.Time
	EntryInputs() []EntryInput
	Annotations() models.Annotations
	AsGroupInput(namespace string, annotations models.Annotations) TransactionGroupInput
}

Jump to

Keyboard shortcuts

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