ledger

package
v1.0.0-beta.236 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 24 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 recreating the original postings from current charge metadata.
  • Collection provenance identifies independently correctable occurrences. Core validators require entries to balance per origin and currency identity.
  • Correction selects remaining account amounts in reverse original collection or backing order. Exact original-entry references bound reversals. Provenance and legacy lineage use the same source-selection policy.
  • Credit-backed earnings recognition consumes only accrued buckets whose source credit and spend charge are both present and distinct. Buckets without that provenance - including invoice-backed accrued value and unbackfilled advances - remain deferred. Unknown-cost promotional credits also remain deferred. Recognition reads origin-tracked accrued buckets directly; legacy recognition maps segments to their original allocation or backfill postings. Both preserve source, spend, currency identity, and tax route. Only the legacy path updates lineage, using the exact amounts selected for posting.

The advance service owns advance creation, backfill, and correction postings. Collector selects correction amounts and unwinds earnings; credit issuance coordinates backfill with settlement and breakage. Callers commit the plans and persist their bookkeeping in the enclosing database transaction.

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.

Collection provenance

Entry provenance carries three distinct identities:

Field Meaning
CollectionOriginID One original collection source slice, including the paired advance issue when applicable
SourceChargeID The charge supplying the value; absent for an uncovered advance
SpendChargeID The charge consuming the value

An origin is minted when credit is collected into accrued or used to cover receivable, or when an advance is issued and collected. Purchase issuance alone does not start a collection origin. Backfill, recognition, correction, and associated breakage releases/reopens preserve the origin. Collecting restored credit again starts another origin, even for the same source and spend charges.

Origins group history; they do not replace source order or exact reversal links. CollectionSource preserves selection order and pairing, while CorrectionSource references the original entry being offset. Amounts come from ledger entries.

Within each transaction, origin-bearing entries must have one non-empty spend charge per origin and currency identity. Transfers between account types must also preserve source attribution. Same-account receivable or accrued translations may attribute an unknown source to a purchase, or reverse that attribution; they cannot transfer attribution between two purchases. Service-level checks under posting locks bound corrections against remaining balances and original-entry reversal capacity.

An absent origin does not by itself imply legacy lineage: purchase issuance, payment, and other unrelated entries also have no collection origin. Legacy collection lifecycle behavior remains in the compatibility path. See the migration guide for deployment and rollback constraints.

Querying provenance

ProvenanceFilter absence means any value; Some(nil) means entries without that ID; Some(&id) matches that ID. Provenance.Filter() pins all three fields, including nils. Balance queries must group by any provenance dimension that needs to remain separate; sharing a subaccount does not imply sharing an origin. OldestMatchingEntryCreatedAt is the earliest creation time among entries matching the bucket query, not an effective-time boundary.

ListTransactions.EntryFilter selects transactions containing a matching entry. By default, each result contains all its entries, including other origins. ReturnOnlyMatchingEntries explicitly returns only the matching entries. Such a result is a transaction view, not necessarily a complete balanced transaction.

Route invariants

CreditFilters holds credit restrictions and matches them against a concrete Route. Feature and plan dimensions combine with AND; entries within a dimension combine with OR. Empty dimensions impose no restriction. Restricted dimensions do not match routes without recorded attribution.

Plans use a catalog key and an optional version comparison: exactly one of eq, in, gte, or lte. Omission matches all versions, including future versions. Spend routes use the immutable plan snapshot on charges and carry an exact version. Credit-source routes carry the grant's restrictions. Collection, advance backfill, and live balance allocation share Matches; Equal compares complete normalized filter sets for bucket identity.

JSON storage uses v1 for features and v2 for plans. Omitted in-memory versions are selected during validation, normalization, and encoding; explicit versions are preserved. Stored JSON requires a supported version and rejects unknown fields. Storage versions do not affect equality or exact route lookup. Legacy grant and route feature columns remain present but unused. Deprecated lineage retains its feature storage; plan-restricted grants cannot match its unattributed advances.

Routes carry currency, credit filters (features and plans), 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.
  • Credit filter 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"
	// AnnotationCustomerBalanceVisibility classifies ledger-only accounting
	// movements that must not be projected as customer credit activity.
	AnnotationCustomerBalanceVisibility = "ledger.customer_balance.visibility"
)
View Source
const (
	BalanceBucketGroupByCollectionOriginID = "collection_origin_id"
	BalanceBucketGroupBySourceChargeID     = "source_charge_id"
	BalanceBucketGroupBySpendChargeID      = "spend_charge_id"
)
View Source
const (
	AnnotationOriginTracked          = "ledger.origin_tracked"
	AnnotationBackfillCreditPriority = "ledger.backfill.credit_priority"
)
View Source
const CollectionTypeBreakage = "breakage"
View Source
const CustomerBalanceVisibilityInternal = "internal"
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 ValidateOriginProvenance

func ValidateOriginProvenance(entries []EntryInput) error

ValidateOriginProvenance prevents a balanced transaction from silently moving value between collection occurrences or dropping its immutable spend identity. Cost-basis translation may add/remove source attribution on the same account type; a transfer between account types must preserve the source as well.

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
	AccountLocker
}

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 {
	OldestMatchingEntryCreatedAt time.Time
	Address                      PostingAddress
	GroupByValues                map[string]*string
	SettledAmount                alpacadecimal.Decimal
	PendingAmount                alpacadecimal.Decimal
}

type BalanceBucketQuery

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

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

func (BalanceBucketQuery) Validate

func (q BalanceBucketQuery) Validate() error

type BalanceQuerier

type BalanceQuerier interface {
	GetBalancesAtBoundaries(ctx context.Context, input GetBalancesAtBoundariesInput) ([]Balance, error)
	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 CreditFilters

type CreditFilters struct {
	Version  CreditFiltersVersion `json:"schema_version"`
	Features []string             `json:"features,omitempty"`
	Plans    []PlanFilter         `json:"plans,omitempty"`
}

CreditFilters combines dimensions with AND and entries within a dimension with OR. An empty dimension imposes no restriction. A restricted dimension does not match a route whose corresponding dimension is absent.

func (CreditFilters) Equal

func (f CreditFilters) Equal(other CreditFilters) bool

func (CreditFilters) IsEmpty

func (f CreditFilters) IsEmpty() bool

func (CreditFilters) MarshalJSON

func (f CreditFilters) MarshalJSON() ([]byte, error)

func (CreditFilters) Matches

func (f CreditFilters) Matches(route Route) bool

Matches compares restrictions with a route's recorded dimensions. This is directional: unrestricted filters match an unattributed route, but a restricted filter does not. It is not exact bucket equality.

func (CreditFilters) Normalize

func (f CreditFilters) Normalize() CreditFilters

func (CreditFilters) String

func (f CreditFilters) String() string

String returns a canonical identity for route pairing, independent of the retained storage version. JSON encoding itself preserves the selected version.

func (*CreditFilters) UnmarshalJSON

func (f *CreditFilters) UnmarshalJSON(data []byte) error

func (CreditFilters) Validate

func (f CreditFilters) Validate() error

type CreditFiltersVersion

type CreditFiltersVersion int
const (
	CreditFiltersVersion1 CreditFiltersVersion = 1
	CreditFiltersVersion2 CreditFiltersVersion = 2
)

type CustomerAccount

type CustomerAccount interface {
	Account
}

CustomerAccount is a Customer specific account

type CustomerAccounts

type CustomerAccounts struct {
	FBOAccount        CustomerFBOAccount
	ReceivableAccount CustomerReceivableAccount
	AccruedAccount    CustomerAccruedAccount
}

func (CustomerAccounts) LockForPosting

func (a CustomerAccounts) LockForPosting(ctx context.Context, locker AccountLocker) error

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
	Filters           CreditFilters
	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
	Filters                        CreditFilters
	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 {
	Provenance
	CollectionSource *string // Custom key to keep 1:1 matching logic during collection
	CorrectionSource *string // References the original entry being reversed
}

func (EntryIdentityParts) Text

type EntryIdentityVersion

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

type EntryInput

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

type EntrySchemaVersion

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

type FeatureFilters

type FeatureFilters []string

func (FeatureFilters) Normalize

func (f FeatureFilters) Normalize() FeatureFilters

func (FeatureFilters) Validate

func (f FeatureFilters) Validate() error

func (FeatureFilters) ValidateAsFeatureFilter

func (f FeatureFilters) ValidateAsFeatureFilter() error

ValidateAsFeatureFilter validates the singular customer-facing filter form. Credit routes may be restricted to multiple features, but a spendability query can only ask for one feature at a time.

type Filters

type Filters struct {
	Provenance ProvenanceFilter
	// 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
	// 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 GetBalancesAtBoundariesInput

type GetBalancesAtBoundariesInput struct {
	Queries []Query
}

GetBalancesAtBoundariesInput describes independent persisted balance boundaries. Results follow Queries order, including zero balances for empty scopes.

func (GetBalancesAtBoundariesInput) Validate

func (i GetBalancesAtBoundariesInput) Validate() error

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

	EntryFilter               TransactionEntryFilter
	ReturnOnlyMatchingEntries bool
	AsOf                      *time.Time

	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 PlanFilter

type PlanFilter struct {
	Key     string         `json:"key"`
	Version *VersionFilter `json:"version,omitempty"`
}

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 Provenance

type Provenance struct {
	CollectionOriginID *string
	SourceChargeID     *string
	SpendChargeID      *string
}

func (Provenance) Filter

func (p Provenance) Filter() ProvenanceFilter

type ProvenanceFilter

type ProvenanceFilter struct {
	CollectionOriginID mo.Option[*string]
	SourceChargeID     mo.Option[*string]
	SpendChargeID      mo.Option[*string]
}

Absent fields match any value; Some(nil) matches entries without that ID.

func (ProvenanceFilter) Validate

func (f ProvenanceFilter) Validate() error

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
	Filters                        CreditFilters
	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 {
	// CreditFilters pins the complete restriction set, including unrestricted
	// dimensions. Features and MatchFeature remain feature-scoped query views.
	CreditFilters     mo.Option[CreditFilters]
	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"
	// V5 adds the complete versioned filter set. Feature-only routes retain
	// their previous key so existing balances are never split by the upgrade.
	RoutingKeyVersionV5 RoutingKeyVersion = "v5"
)

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 {
	GroupID() models.NamespacedID
	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 TransactionEntryFilter

type TransactionEntryFilter struct {
	AccountIDs []string
	Currency   *currencyx.Code
	Route      RouteFilter
	Provenance ProvenanceFilter
}

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
}

type VersionFilter

type VersionFilter struct {
	Eq  *int  `json:"eq,omitempty"`
	In  []int `json:"in,omitempty"`
	Gte *int  `json:"gte,omitempty"`
	Lte *int  `json:"lte,omitempty"`
}

VersionFilter uses one integer comparison. Omission matches every version, including versions published after the grant was created.

func (VersionFilter) Validate

func (f VersionFilter) Validate() error

Jump to

Keyboard shortcuts

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