Documentation
¶
Overview ¶
manager.go is the PER-TENANT selector over the treasury Store: it resolves each request to its OWN Hanzo Base (SQLite) file instead of a process-wide singleton, so one tenant's finance/ledger writes can NEVER appear in another tenant's reads. This is the storage side of the standing Hanzo rule — Postgres stays a supported option (Formance, one layer up), but every tenant's books run live on its own Base file.
TWO file classes, one opener:
- the HOUSE ledger — the platform's OWN reserve fund (fund:reserve, revenue:*, payout:*): a SINGLE single-writer, overdraw-guarded file. It CANNOT be split per tenant (the reserve overdraw guard is one atomic balance), so it is one fixed file — the pre-existing {DataDir}/treasury.db, kept verbatim so live reserve capital is never orphaned by a path change.
- a CUSTOMER ledger — one isolated file per tenant at {DataDir}/finance/{slug}.db, opened on first use and cached.
It CONSUMES the treasury's canonical Base opener (Open, over github.com/hanzoai/ sqlite — the ONE Hanzo Base driver) rather than importing the ledger's per-tenant opener: that opener is a test-only helper deliberately kept unexported because it registers modernc's database/sql "sqlite" driver, which would COLLIDE with the hanzoai/sqlite registration this store already carries. the ledger's own contract is "production callers supply their own *bun.DB to New" — which Open does — so the per-tenant file selection lives HERE, over Open, and the slug guard matches the ledger's per-tenant guard exactly.
Package sqlstore is the Hanzo Base (HIP-0105 per-tenant SQLite) adapter for the ledger core: it implements ledger.Store + ledger.Tx over one SQLite file, and nothing more. It carries the storage concern the core deliberately does not — the SAME single-connection + WAL pattern every clients/* store uses (referrals, crm, prompts), so it is Base-compatible and drops into the unified binary unchanged.
It imports the core (ledger) and the one Hanzo SQLite driver — never cloud, zip, or IAM. When the core is lifted to hanzoai/finance this adapter travels with it as the default backend; the driver import is the only thing a different Base backend would swap.
MONEY IS EXACT AND BIG. Amounts are 18-decimal USD (1e-18, the EVM/uint256 unit) held as big.Int money.Amount — a value exceeds SQLite's 64-bit INTEGER past ~$9.20, so amount columns are TEXT (the signed 18-decimal integer string) and an account's balance is a maintained running total (treasury_accounts.balance), NOT a SQL SUM (you cannot SUM a decimal-string column, and a busy wallet's million usage postings must not be re-summed on every gate read). The running balance is updated inside the same transaction as each posting, so it can never drift from the journal.
Index ¶
- Constants
- type Manager
- type Store
- func (s *Store) Balance(ctx context.Context, account string) (money.Amount, error)
- func (s *Store) BalancesWithPrefix(ctx context.Context, prefix string) (map[string]money.Amount, error)
- func (s *Store) Close() error
- func (s *Store) Entries(ctx context.Context, limit int) ([]ledger.JournalEntry, error)
- func (s *Store) Policy(ctx context.Context) (ledger.SharePolicy, error)
- func (s *Store) SetPolicy(ctx context.Context, p ledger.SharePolicy) error
- func (s *Store) SumByKindSince(ctx context.Context, kind string, since int64) (money.Amount, error)
- func (s *Store) Tx(ctx context.Context, fn func(ledger.Tx) error) error
Constants ¶
const HouseSlug = "house"
HouseSlug names the platform's OWN reserve/house ledger — the single (single-writer, overdraw-guarded) book behind every backed payout. It is RESERVED: tenantSlug routes a literal "house" tenant to a hashed slug, so a caller can never open the house fund by naming its org "house".
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager opens and caches one *Store per tenant. It is safe for concurrent use. Each distinct tenant maps to a distinct file; the mapping is INJECTIVE (it never folds "acme" and "ACME" into one bucket — that would itself be a cross-tenant break) and can never traverse the path or collide with the reserved house file.
func NewManager ¶
NewManager roots the per-tenant treasury stores under dataDir: the house ledger at {dataDir}/treasury.db (preserved) and customer ledgers under {dataDir}/finance/. The finance dir (and thus dataDir) is created if absent.
func (*Manager) Close ¶
Close closes every open store (house + tenants). Idempotent; returns the first close error, if any.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the SQLite-backed ledger persistence. ONE file holds the whole chart of accounts, journal and policy for a deployment (the platform's own books — not per-org, unlike a tenant product store). Serialized on a single connection so the engine's balance-guard read-then-write is atomic under load.
func Open ¶
Open opens (creating + migrating) the ledger database at path, under the principal its key is bound to.
The principal is a PARAMETER because these stores do not share one. A per-org ledger (orgs/<org>/finance.db) is bound to that org; the platform's own treasury.db is bound to Global. This function used to hard-code Global for both — and cek-rewrap had already moved every per-org sidecar to its owner ("a store's key names its owner", 2026-07-31 01:25). So the data was migrated correctly and the opener was never updated: every per-org ledger failed to unwrap with "wrong key, wrong principal, or corrupt blob".
That read as data loss and was not: 86 finance ledgers were intact and correctly keyed the whole time. Because the AI balance gate is fail-closed and could not read a balance, it refused EVERY completion — chat, copilot and documents — fleet-wide. Naming the principal at the call site is what stops an opener and a migration from disagreeing again, because now they cannot both be silent about it.
func (*Store) BalancesWithPrefix ¶
func (*Store) SumByKindSince ¶
SumByKindSince sums the entry amounts of ONE kind created at/after `since` (unix seconds) — a read-only aggregate over the indexed created_at, no schema change. Amounts are 18-decimal TEXT (SQLite INTEGER overflows past ~$9.20), so the fold is in Go. This is the usage-cap's period-spend source: sum kind "finance.usage" for the org store since the start of the UTC month, so a cap enforces on real ledger spend rather than a bounded, truncatable journal scan.