finance

package
v1.801.490 Latest Latest
Warning

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

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

Documentation

Overview

Package finance is the prepaid wallet your org pays from: deposits in, usage debits out, always balanced.

It is a per-CUSTOMER, double-entry balance on the native ledger core (apps/treasury/ledger, the same engine the platform reserve posts to). It registers NO routes and NO ops — it is the in-process implementation of cloud's types.FinanceClient (package alias finance.Client, mirroring commerce.Client), the ONE money seam the ai prepaid gate, the admin grant, commerce's credit mint and the edge meter all bill through; billing is the customer-facing door onto it.

ONE LIGHTWEIGHT FILE PER ORG. Each org's books are an isolated Hanzo Base (SQLite) file at <dataDir>/orgs/<org>/finance.db (a separate <...>/finance-test.db for sandbox money, so test and live never mix). There is NO shared or relational database — the file IS the tenant boundary, so one org's wallet writes can never appear in another org's read. A file opens on first use and is cached; opens are serialized so a concurrent first touch opens exactly once.

MONEY-SAFETY. Every write is a BALANCED double-entry posting inside the org's own file: a deposit is funding:platform → wallet (credit the customer, debit the platform float); a usage debit is wallet → revenue:platform (debit the customer, credit platform revenue) — so every customer debit IS a platform-revenue credit in one atomic entry, and a file's postings always sum to zero. Both writes are idempotent on their ref (the ledger's (kind,program,ref) idempotency): a usage debit on UsageInput.Ref and a deposit on DepositInput.Ref, so a retried debit or a fixed-ref backfill posts AT MOST ONCE; an empty Ref takes a fresh, server-minted one and stays additive (grants stack, metered calls each bill). A usage ref is unique WITHIN THE WALLET it debits, so one subject's key can never swallow another's, and a ref that already names a posting of a DIFFERENT amount is refused rather than answered with the first one's entry — same key is not same act. Amounts are int64 minor units (USD cents) — no float ever touches a balance. A balance read is the settled ledger balance, clamped at zero; transient holds are the caller's in-pod concern, never persisted here.

Index

Constants

This section is empty.

Variables

View Source
var ErrRefReused = errors.New("names a different charge")

ErrRefReused is a usage ref that already names a DIFFERENT charge in this wallet. Answered rather than silently deduped: a key that means "this is the act I already paid for" is a lie when the amount differs, and answering the first entry would hand back free work for the price of a repeated key.

EXPORTED because a surface that lets a caller NAME the act — the GPU charge — has to tell that caller its key is taken, and a conflict rendered as a billing outage sends it into a retry that will never clear. It is the usage twin of the deposit's own refusal, so the money plane answers a reused key one way.

View Source
var ErrRefTaken = errors.New("already used for a different (subject,amount)")

ErrRefTaken is what a deposit gets when its Ref is ALREADY posted for a different (subject, amount): the ref names a payment that is not this one.

It is an error and not an entry, and that is the whole finding. The idempotency key is (kind, program, ref) — no subject, no amount — so any two deposits sharing a ref in one org's books collide, and the replay branch answered the SECOND of them with the FIRST one's entry id. The caller was told SUCCESS for money that was never credited: alice's $1 posted, bob's $500 returned alice's entry, bob's wallet stayed at zero and nothing anywhere said so. Today the settlement ref is a Square payment id and no two payments share one, which is the only reason this has never fired; a webhook replay, a backfill or the credit RPC choosing a ref of its own is all it takes.

A conflict cannot be resolved here. Crediting anyway would break the exactly-once the ref exists to give, and answering with the other payment's entry is the swallow itself. So it is refused, loudly, and the caller picks a ref that is its own.

EXPORTED for the reason ErrRefReused is, and it is the deposit twin of exactly that: a conflict is the ONE deposit failure no retry can clear, so a caller that renders it as a transient billing fault sends a customer round a loop that cannot end. The credit door tells the two apart to say whether its refusal is terminal (commerce settle.go), and the backfill reads its own fixed ref back through it (migrate.go). One error, one meaning, both sides of the money plane.

Functions

func MigrateOrg

func MigrateOrg(ctx context.Context, org string, balanceCents int64) (string, error)

MigrateOrg carries an org's existing prepaid balance into its native finance wallet, EXACTLY ONCE — the cutover primitive that moves each tenant's money from the legacy commerce ledger onto the ONE finance ledger. It deposits balanceCents into org's pooled wallet (subject == the org slug) under the FIXED idempotency ref "backfill:<org>", so a re-run (a retried operator call, a replayed job) finds the existing entry and credits the wallet at most once — returning the original entry id, moving no money. A non-positive balance is skipped ("" , nil): there is nothing to carry. It resolves the process-wide finance client published at boot; a nil client (finance not co-resident on this deployment) is an error, since there is no wallet to migrate into.

A re-run is a no-op whatever the legacy balance says by then

The ref is fixed and the AMOUNT IS NOT, and that is the whole of the second read below. finance's own idempotency answers a replay with the first entry only when the ref names the SAME (subject, amount) — a rule that is exactly right for a settlement, where a ref is one payment and a differing amount means a DIFFERENT payment wearing a taken key (ErrRefTaken). A backfill is not that. Its ref names an ORG's one cutover, its amount is whatever commerce happened to hold when somebody ran it, and that figure moves: the customer spends, tops up, is granted credit. So the honest re-run — an operator running the cutover twice over a balance that changed in between — collided with the conflict rule and was answered with an error, breaking the exactly-once this function exists to promise and making a retried cutover look like a failed one.

It is read back instead: the ref is THIS org's own and nothing else may write it, so a conflict on it means the carry already happened. The entry it happened under is answered, no money moves, and the answer is the same on every subsequent run — which is what "exactly once" has to mean for a primitive whose input is a moving number. The conflict rule itself is untouched: a SETTLEMENT ref that is another payment's is still refused, loudly, everywhere else.

func New

func New(dataDir string) *ledgerFinance

New returns a finance client rooting each org's prepaid wallet ledger under dataDir, in that org's own namespace ("finance", or "finance-test" in sandbox mode). Files open lazily on first use.

func Publish

func Publish(c Client)

Publish records the process-wide finance client. Called once at boot; nil clears.

func SetUsageHook

func SetUsageHook(h func(org string, test bool, project, service string))

SetUsageHook installs the post-debit hook (the cap alert-fire). Pass nil to clear.

Types

type Client

type Client = types.FinanceClient

Client is the in-process inter-subsystem seam cloud's money paths call. It IS cloud's types.FinanceClient — one narrow interface (BalanceCents + Deposit + RecordUsage), kept as an alias so a value satisfies both names with no adapter (mirrors commerce.Client).

func Current

func Current() Client

Current returns the published finance client, or nil before Publish (a consumer treats nil as "money layer not co-resident" and fails closed).

type Kind added in v1.801.350

type Kind string

Kind classifies a wallet entry's economic meaning — money IN or money OUT — and is also the first half of the entry's idempotency key. Kept small and closed: two directions, two names.

IT IS THE ONE VOCABULARY, and it is a TYPE rather than a pair of strings because both halves of the money surface read it. The ledger WRITES these kinds and the customer's finance pages CLASSIFY on them, and while the reader held its own string literals the two silently disagreed: the reader matched commerce's `deposit` / `withdraw` against entries this file has always written as `finance.deposit` / `finance.usage`, so over the peer path a customer's credits rendered empty, their usage totalled zero, and their own top-up signed NEGATIVE. Nothing failed — the strings just never met. Named constants of a named type make that disagreement a compile error instead of a wrong number on a customer's balance page.

const (
	KindDeposit Kind = "finance.deposit" // funding:platform → wallet (a prepaid grant/settlement)
	KindUsage   Kind = "finance.usage"   // wallet → revenue:platform (a metered debit)
	// KindUnknown is what a kind the ledger never wrote parses to. A reader treats it
	// as neither direction rather than guessing one — an entry nobody can classify
	// must not be silently counted as spend.
	KindUnknown Kind = ""
)

func ParseKind added in v1.801.350

func ParseKind(s string) Kind

ParseKind reads a kind back off a wire — the internal plane's Txn.Kind, or a row re-read from an older store. It is the ONE place the ledger's spellings are recognized, so a reader never compares an entry to a string literal.

type TxnRow added in v1.801.350

type TxnRow struct {
	ID        string
	Kind      Kind
	Ref       string
	Memo      string
	Amount    money.Amount
	CreatedAt int64
}

TxnRow is one ledger entry as the billing surface reads it: every kind, not only the usage debits ListUsage keeps. Credits, grants and debits are all transactions to a customer looking at their account, and the three finance pages (credits/usage/ledger) are three projections of THIS one list.

Amount stays the ledger's own money.Amount — 18-decimal exact, the same integer an on-chain balance holds — because this is where that value lives. Flattening to cents here would round away everything below a cent, which is most of what a per-token AI price IS.

Kind is the ledger's OWN Kind, not a string: a customer-facing page decides whether a row is money in or money out by comparing it, and a reader holding its own string literals is how the credits page came to render empty against a ledger full of grants.

type UsageRow

type UsageRow struct {
	ID    string `json:"id"`
	Cents int64  `json:"cents"`
	// Amount is the debit exactly as the ledger holds it — the same 18-decimal
	// value the wallet→revenue posting moved. Cents beside it is the ROUNDING of
	// this value, kept for the wires that already carry cents; it is never the
	// source. TxnRow below has said why since it was written: "Flattening to
	// cents here would round away everything below a cent, which is most of what
	// a per-token AI price IS" — and this row, read from the SAME ledger, was
	// flattening. A customer whose usage was a thousand sub-cent calls read back
	// a page of zeros that summed to zero.
	Amount    money.Amount `json:"amount"`
	Model     string       `json:"model"`
	CreatedAt int64        `json:"createdAt"`
}

UsageRow is one recorded usage debit — the READ twin of RecordUsage. The SAME wallet→revenue posting a metered call wrote is read back here, so the usage a customer SEES is exactly what drained their wallet. Cents is the debit magnitude (USD minor units); Model is the metered-unit label the debit carried (Entry.Memo); CreatedAt is unix seconds.

Jump to

Keyboard shortcuts

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