books

package
v1.801.307 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package books is the revenue BOOKS spine: a native double-entry ledger that records hanzo.ai's real prepaid-credit revenue on per-org Base/SQLite, exposed at /v1/books.

WHY THIS EXISTS. commerce holds the money (a prepaid wallet: deposits + withdraws); finance.go PROJECTS that wallet for the customer UI. Neither keeps BOOKS — a double-entry general ledger with a chart of accounts, revenue recognition, and a trial balance that proves the books balance. This domain ports ERPNext's Accounts SEMANTICS (process_gl_map: merge → toggle → round-off → the debit==credit invariant) to Go, with ZERO of its Python/Postgres, and books commerce's transactions into it.

THE ONE POSTING SOURCE. commerce GET /v1/billing/transactions is the SOLE source (ingest.go). This domain is READ-ONLY against commerce — it never mints a deposit, credit, or payout. It only READS money that already moved and writes the accounting twin. So the books can restate but never create money.

TENANT ISOLATION. Every read resolves the caller's OWN org from the validated principal (principal.Org — the gateway-minted X-Org-Id, HIP-0026), and each org's books live in a physically separate {DataDir}/orgs/{slug}/books.db (sandbox → books-sandbox.db). One org can never read another's ledger.

Index

Constants

View Source
const (
	Bank            = "1000" // operating cash
	SquareClearing  = "1010" // funds captured by Square, pre-settlement
	AR              = "1200" // accounts receivable (party)
	OwnerEquity     = "3000" // owner equity
	RoundOff        = "3900" // round-off difference sink (see Post)
	CustomerWallet  = "2000" // PREPAID CREDITS — a liability / deferred revenue
	VendorPayable   = "2001" // accounts payable to a vendor for a scanned bill (party)
	OSSPayout       = "2100" // owed to OSS maintainers (party)
	SalesTaxPayable = "2200" // collected sales tax owed to authorities (party)
	AccruedInfra    = "2300" // accrued cloud/GPU infra cost owed (COGS accrual sink)
	UsageRevenue    = "4000" // AI usage revenue — RECOGNIZED on consumption
	MRR             = "4100" // recurring subscription revenue
	ProductRevenue  = "4200" // one-off product revenue
	CloudCOGS       = "5000" // cloud / GPU cost of goods sold
	ProcessorFees   = "5100" // payment-processor fees
	PromoCredit     = "5200" // promotional credit given away
	// Scanner expense categories — the fixed buckets a scanned bill (scan.go) maps its
	// merchant/category into. Each is a real debit-normal expense account so a booked bill
	// always lands in a named line, never a guessed one; an unrecognized category falls to
	// 5900 Uncategorized (below) for a human to reclassify.
	SoftwareExpense  = "5300" // software & SaaS subscriptions
	OfficeExpense    = "5400" // office supplies & equipment
	TravelExpense    = "5500" // travel & transport
	MealsExpense     = "5600" // meals & entertainment
	MarketingExpense = "5700" // marketing & advertising
	ServicesExpense  = "5800" // professional & contractor services
	// UncategorizedExpense is the DEFAULT debit sink for a bank OUTFLOW the bank
	// engine could not categorize (bank_map.go). It exists so an outflow always
	// books against a real expense account — never a guessed one — and surfaces as
	// a bucket a human recategorizes, rather than silently landing in COGS.
	UncategorizedExpense = "5900" // outflow pending human categorization
)

Account numbers — the ONE set of posting keys the rule map and reports reference.

View Source
const RoundOffAllowance int64 = 2

RoundOffAllowance bounds the debit−credit difference process gl will absorb into the round-off account instead of rejecting. With exact integer cents a well-formed voucher nets to zero, so this only ever soaks up a 1–2¢ artifact of an upstream split; a larger gap is a real imbalance and MUST fail closed rather than silently plug equity.

Variables

This section is empty.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount opens the per-org book stores, wires the commerce posting source, and registers the /v1/books surface.

func Shutdown

func Shutdown() error

Shutdown closes every open per-org book store (live + sandbox).

Types

type Account

type Account struct {
	Number string      `json:"number"`
	Name   string      `json:"name"`
	Type   AccountType `json:"type"`
	Party  PartyType   `json:"party,omitempty"`
}

Account is one line of the chart: a stable number (the posting key), a human name, its fundamental type, and — for AR/AP — its party subledger class.

type AccountType

type AccountType string

AccountType is the fundamental accounting class of an account. It records the account's NORMAL balance side (asset/expense are debit-normal; liability/income/equity are credit-normal) for classification and presentation; the trial balance places a signed net by its SIGN, so a faithfully-kept ledger never depends on the normal side to balance.

const (
	Asset     AccountType = "asset"
	Liability AccountType = "liability"
	Income    AccountType = "income"
	Expense   AccountType = "expense"
	Equity    AccountType = "equity"
)

type AskRequest

type AskRequest struct {
	Question string `json:"question"`
	// From/To optionally scope the metric window (RFC3339). Empty = all-time, treated as a
	// single reporting period (see monthsBetween).
	From string `json:"from,omitempty"`
	To   string `json:"to,omitempty"`
}

AskRequest is the POST /v1/books/ask body.

type AskResponse

type AskResponse struct {
	Answer    string   `json:"answer"`
	Figures   []Figure `json:"figures"`
	Followups []string `json:"followups"`
	Sources   []string `json:"sources"`
}

AskResponse is the Ask contract: a natural-language answer grounded in Figures, with followup questions and the report Sources the figures were computed from.

type BalanceLine

type BalanceLine struct {
	Account string      `json:"account,omitempty"`
	Name    string      `json:"name"`
	Type    AccountType `json:"type,omitempty"`
	Amount  int64       `json:"amount"` // cents, display sign
}

BalanceLine is one line of the sheet, in NATURAL (positive in normal operation) sign. A derived line (retained earnings) carries no account number.

type BalanceSheet

type BalanceSheet struct {
	AsOf             string        `json:"asOf,omitempty"`
	Assets           []BalanceLine `json:"assets"`
	Liabilities      []BalanceLine `json:"liabilities"`
	Equity           []BalanceLine `json:"equity"`
	TotalAssets      int64         `json:"totalAssets"`
	TotalLiabilities int64         `json:"totalLiabilities"`
	TotalEquity      int64         `json:"totalEquity"`
	Balanced         bool          `json:"balanced"` // TotalAssets == TotalLiabilities + TotalEquity
}

BalanceSheet is the point-in-time statement as of a posting time, with the equation proof.

type BankQuestion

type BankQuestion struct {
	Connector  string `json:"connector"`
	ExternalID string `json:"externalId"`
	Prompt     string `json:"prompt"`
	Status     string `json:"status"`
	CreatedAt  string `json:"createdAt"`
}

BankQuestion is one open clarifying question about an unmatched bank inflow — the persistence row. It adapts to the founder-facing canonical Question (anomalies.go) via [BankQuestion.toQuestion] so the one /v1/books/questions surface can present anomaly and bank questions through a single type.

type BankResult

type BankResult struct {
	Status         string `json:"status"`
	VoucherPosted  bool   `json:"voucherPosted"`
	QuestionRaised bool   `json:"questionRaised"`
	Skipped        bool   `json:"skipped"` // already processed (idempotent no-op)
}

BankResult reports what mapAndPost did with one transaction — for the sync/import response tallies and for tests to assert the taken path.

type BankTally

type BankTally struct {
	Ingested   int `json:"ingested"`   // transactions seen
	Posted     int `json:"posted"`     // vouchers newly posted (outflow + reconciled)
	Reconciled int `json:"reconciled"` // inflows cleared against Square-clearing
	Questions  int `json:"questions"`  // unmatched inflows that raised a question
	Transfers  int `json:"transfers"`  // own-account moves recorded (no P&L)
	Skipped    int `json:"skipped"`    // already-processed idempotent no-ops
}

BankTally is the per-request summary of a bank ingest (import or sync).

type BankTxn

type BankTxn struct {
	Connector   string    `json:"connector"`
	ExternalID  string    `json:"externalId"`
	PostedAt    string    `json:"postedAt"` // RFC3339 posting time, ledger-comparable
	AmountCents int64     `json:"amountCents"`
	Currency    string    `json:"currency"`
	Description string    `json:"description"`
	Merchant    string    `json:"merchant"`
	Direction   Direction `json:"direction"`
}

BankTxn is the normalized bank transaction — the single shape every connector emits and the bank engine consumes. Connector + ExternalID is the idempotency key: the same statement row, re-imported or re-synced, always carries the same pair, so it maps and posts exactly once.

type BankTxnRow

type BankTxnRow struct {
	Connector      string    `json:"connector"`
	ExternalID     string    `json:"externalId"`
	PostedAt       string    `json:"postedAt"`
	AmountCents    int64     `json:"amountCents"`
	Currency       string    `json:"currency"`
	Direction      Direction `json:"direction"`
	Description    string    `json:"description,omitempty"`
	Merchant       string    `json:"merchant,omitempty"`
	MatchedVoucher string    `json:"matchedVoucher,omitempty"`
	Status         string    `json:"status"`
}

BankTxnRow is a persisted bank_txn as the read API surfaces it.

type BookRequest

type BookRequest struct {
	ScanID  string  `json:"scanId"`
	Voucher Voucher `json:"voucher"`
	// Override books this bill even when one of the SAME economic identity
	// (vendor, total, issue date) already posted — the explicit human confirmation that a
	// same-looking bill is a genuine second spend, not the same receipt re-scanned.
	Override bool `json:"override,omitempty"`
}

BookRequest is the POST /v1/books/scan/book body: the reviewed voucher (the frontend may have edited any field) plus the ScanID that keys idempotency. SourceKind/SourceID are FORCED server-side to (scan, ScanID) so a client can never post a scan under another source's key or double-book by editing the id.

type BookResponse

type BookResponse struct {
	ScanID string `json:"scanId"`
	Posted bool   `json:"posted"`
}

BookResponse reports the outcome of a scan book: posted=false means the same scan already booked (idempotent no-op), never a second voucher.

type Connector

type Connector interface {
	Name() string
	Fetch(ctx context.Context, org, cursor string) (txns []BankTxn, nextCursor string, err error)
}

Connector is the pull seam to one bank source. Name is the stable connector id (the bank_txn.connector / cursor key). Fetch returns the batch of transactions AFTER cursor plus the nextCursor to persist; a connector resolves its own credentials from KMS inside Fetch and NEVER returns or logs them. A returned nextCursor equal to the input cursor means "no advance" (nothing new).

type Direction

type Direction string

Direction is a bank transaction's money direction relative to our account. It pairs with the (always non-negative) AmountCents magnitude: an Outflow is money leaving the account (an expense), an Inflow is money arriving (a deposit — reconciled or new), and a Transfer is a move between our OWN accounts with no profit-and-loss effect.

const (
	Inflow   Direction = "inflow"
	Outflow  Direction = "outflow"
	Transfer Direction = "transfer"
)

type Extracted

type Extracted struct {
	Merchant   string     `json:"merchant"`
	IssuedAt   string     `json:"issuedAt"` // YYYY-MM-DD
	TotalCents int64      `json:"totalCents"`
	TaxCents   int64      `json:"taxCents"`
	Currency   string     `json:"currency"`
	Category   string     `json:"category"` // proposed slug (software|cloud|office|…)
	LineItems  []LineItem `json:"lineItems,omitempty"`
	Note       string     `json:"note,omitempty"`
}

Extracted is the structured shape the AI extracts from a receipt/invoice. Amounts are exact int64 cents (the AI is instructed to return integer cents, never a decimal), so no float rounding can enter downstream. Category is the AI's proposed slug — a HINT that vendor/rule resolution overrides when it knows better.

type Figure

type Figure struct {
	Label  string `json:"label"`
	Value  string `json:"value"`
	Period string `json:"period,omitempty"`
}

Figure is one grounded number in the answer: a label, its formatted value, and the period it covers. This is the exact shape the Ask contract fixes.

type FinancialPackage

type FinancialPackage struct {
	Org          string       `json:"org"`
	From         string       `json:"from,omitempty"`
	To           string       `json:"to,omitempty"`
	GeneratedAt  string       `json:"generatedAt"`
	TrialBalance TrialBalance `json:"trialBalance"`
	PnL          PnL          `json:"pnl"`
	BalanceSheet BalanceSheet `json:"balanceSheet"`
	GL           []GLRow      `json:"gl"`
}

FinancialPackage is the export bundle over a [From, To] period. GLLimit rows of the most recent GL detail are included as the audit trail behind the statements.

type GLRow

type GLRow struct {
	ID         int64  `json:"id"`
	PostingAt  string `json:"postingAt"`
	Account    string `json:"account"`
	Debit      int64  `json:"debit"`
	Credit     int64  `json:"credit"`
	Against    string `json:"against,omitempty"`
	SourceKind string `json:"sourceKind"`
	SourceID   string `json:"sourceId"`
	Remarks    string `json:"remarks,omitempty"`
}

GLRow is one persisted GL Entry, as the read API surfaces it.

type Importer

type Importer interface {
	Parse(data []byte) ([]BankTxn, error)
}

Importer is the OPTIONAL capability a push-based connector (OFX/QFX/CSV import) adds on top of Connector: it parses an uploaded file body into BankTxns. The import route type-asserts to it, so a connector that has not yet implemented parsing simply is not an Importer and the route reports "not yet available" rather than mis-handling the upload.

type InboxItem

type InboxItem struct {
	ID         string     `json:"id"` // the file hash (== a scan's ScanID)
	Filename   string     `json:"filename,omitempty"`
	Status     string     `json:"status"`
	CreatedAt  string     `json:"createdAt"`
	Extracted  *Extracted `json:"extracted,omitempty"`
	Vendor     string     `json:"vendor,omitempty"`
	Category   string     `json:"category,omitempty"`
	Confidence string     `json:"confidence,omitempty"`
}

InboxItem is one queued document as GET /v1/books/inbox surfaces it: its hash id, filename, status, and — once scanned — the extracted summary and its confidence.

type Leg

type Leg struct {
	Account string `json:"account"`
	Debit   int64  `json:"debit"`
	Credit  int64  `json:"credit"`
}

Leg is one side of a posting: an account and its debit AND credit in cents. A caller sets exactly one of the two; the pipeline normalizes anything else (a merge can leave both set, which toggle collapses to a single side).

type LineItem

type LineItem struct {
	Description string `json:"description"`
	AmountCents int64  `json:"amountCents"`
}

LineItem is one line of a scanned document — a description and its amount in exact cents.

type Metrics

type Metrics struct {
	From            string `json:"from,omitempty"`
	To              string `json:"to,omitempty"`
	Period          string `json:"period"`          // human label, e.g. "2026-07" or "all-time"
	Months          int    `json:"months"`          // window length used to normalize MRR / burn
	MRR             int64  `json:"mrr"`             // recurring revenue (4100) per month
	ARR             int64  `json:"arr"`             // MRR * 12
	Revenue         int64  `json:"revenue"`         // recognized revenue (all Income roots) over the period
	COGS            int64  `json:"cogs"`            // cost of goods (5000) over the period
	Burn            int64  `json:"burn"`            // total expense (all Expense roots) over the period
	GrossProfit     int64  `json:"grossProfit"`     // Revenue − COGS
	GrossMarginBps  int64  `json:"grossMarginBps"`  // GrossProfit / Revenue in basis points (7000 = 70%)
	NetIncome       int64  `json:"netIncome"`       // Revenue − Burn
	Cash            int64  `json:"cash"`            // Bank (1000) + Square clearing (1010) as of `to`
	DeferredRevenue int64  `json:"deferredRevenue"` // Customer Wallet (2000) liability as of `to`
	MonthlyBurn     int64  `json:"monthlyBurn"`     // net cash burned per month (>0 ⇒ losing cash)
	RunwayMonths    int64  `json:"runwayMonths"`    // Cash / MonthlyBurn; -1 = infinite (not burning)
}

Metrics is the deterministic SaaS-metrics snapshot over a reporting window. Every field is int64 CENTS except the two ratios/counters (basis points, months), matching the ledger's exact-integer money model. RunwayMonths == -1 is the INFINITE sentinel (the org is not burning cash), never a real month count.

type MetricsResponse

type MetricsResponse struct {
	Metrics
	Figures []Figure `json:"figures"`
}

MetricsResponse is the GET /v1/books/metrics payload: the raw deterministic snapshot (int64 cents, the canonical numbers) PLUS the same figures already formatted through the ONE money formatter (formatUSD), so a consumer surfaces books' numbers in books' format and never re-derives either. It is the single grounded read the unified /v1/ask advisor composes — every figure is the ledger, aggregated, never a guess.

type PartyType

type PartyType string

PartyType marks an account that carries a SUBLEDGER (the Payment Ledger Entry twin): a receivable (money owed TO us) or a payable (money we owe). A leg posted to such an account also writes a payment_ledger_entry row, mirroring ERPNext's party-account posting. NoParty accounts (bank, wallet, revenue, COGS) carry no subledger.

const (
	NoParty    PartyType = ""
	Receivable PartyType = "receivable"
	Payable    PartyType = "payable"
)

type PnL

type PnL struct {
	From         string    `json:"from,omitempty"`
	To           string    `json:"to,omitempty"`
	Income       []PnLLine `json:"income"`
	Expense      []PnLLine `json:"expense"`
	TotalIncome  int64     `json:"totalIncome"`
	TotalExpense int64     `json:"totalExpense"`
	NetIncome    int64     `json:"netIncome"` // TotalIncome − TotalExpense
}

PnL is the period income statement: income lines, expense lines, and the net. The period is (From, To] — movement strictly after From, up to and including To — matching the trial balance's opening/closing boundary convention (from exclusive).

type PnLLine

type PnLLine struct {
	Account string      `json:"account"`
	Name    string      `json:"name"`
	Type    AccountType `json:"type"`
	Amount  int64       `json:"amount"` // cents, display sign (income & expense both positive when normal)
}

PnLLine is one account's contribution to the statement, shown in its NATURAL (positive in normal operation) sign: income as its credit balance, expense as its debit balance.

type Question

type Question struct {
	ID       string `json:"id"`     // the source transaction id it concerns
	Kind     string `json:"kind"`   // outlier|reversal|roundoff|uncosted|overdrawn
	Text     string `json:"text"`   // the specific question to ask the founder
	Amount   string `json:"amount"` // formatted figure ($…)
	Account  string `json:"account,omitempty"`
	PostedAt string `json:"postedAt,omitempty"`
}

Question is one clarifying question about an unusual transaction: the sharp text, the formatted amount that makes it concrete, and the source/account/time that anchor it.

type QuestionsResponse

type QuestionsResponse struct {
	Questions []Question `json:"questions"`
}

QuestionsResponse is the GET /v1/books/questions payload.

type Rule

type Rule struct {
	Pattern  string `json:"pattern"`
	Category string `json:"category"` // COA account number
	Priority int    `json:"priority"`
}

Rule is one categorization rule: a merchant substring pattern, the category (COA account) it books to, and a priority (higher wins when several patterns match).

type ScanDraft

type ScanDraft struct {
	ScanID     string     `json:"scanId"` // file hash — the (scan, id) idempotency key
	Extracted  Extracted  `json:"extracted"`
	Vendor     string     `json:"vendor"`
	Category   string     `json:"category"`   // resolved COA expense account number
	Confidence string     `json:"confidence"` // auto | low
	Voucher    Voucher    `json:"voucher"`    // PROPOSED — not yet posted
	Balanced   bool       `json:"balanced"`   // Σdebit == Σcredit (always true when built)
	Questions  []Question `json:"questions,omitempty"`
}

ScanDraft is the POST /v1/books/scan response: the extracted fields plus a PROPOSED balanced voucher that has NOT been posted. Confidence is "auto" when a vendor/rule resolved the category, else "low" — in which case Questions carries a clarifying prompt and the frontend should confirm the category (and can persist a rule) before booking.

type TellerLink struct {
	ApplicationID string `json:"applicationId"`
	Environment   string `json:"environment"`
}

TellerLink is the client-side config for Teller Connect (the browser widget that produces an enrollment). application_id is PUBLIC (not a secret); environment selects sandbox vs production. This is the link-token analog — Teller Connect runs client-side, so there is no server-minted token, only this public config.

type TrialBalance

type TrialBalance struct {
	From        string            `json:"from,omitempty"`
	To          string            `json:"to,omitempty"`
	Rows        []TrialBalanceRow `json:"rows"`
	TotalDebit  int64             `json:"totalDebit"`
	TotalCredit int64             `json:"totalCredit"`
	Balanced    bool              `json:"balanced"`
}

TrialBalance is the whole-ledger report: per-account rows + totals + the balance proof.

type TrialBalanceRow

type TrialBalanceRow struct {
	Account       string      `json:"account"`
	Name          string      `json:"name"`
	Type          AccountType `json:"type"`
	OpeningDebit  int64       `json:"openingDebit"`
	OpeningCredit int64       `json:"openingCredit"`
	Debit         int64       `json:"debit"`  // period movement
	Credit        int64       `json:"credit"` // period movement
	ClosingDebit  int64       `json:"closingDebit"`
	ClosingCredit int64       `json:"closingCredit"`
}

TrialBalanceRow is one account's line: opening + period movement → closing, each split onto its debit/credit column by the SIGN of its net (debit − credit): a positive net is a debit balance, a negative net a credit balance. Type is carried for presentation (the account's normal side) but does not affect placement — a faithfully-signed net is shown truthfully, so a contra-balance (e.g. an overdrawn wallet) reads as it really is.

type Txn

type Txn struct {
	Date         string `json:"date"`
	Description  string `json:"description"`
	Vendor       string `json:"vendor,omitempty"`
	Category     string `json:"category"` // COA account number of the P&L line
	CategoryName string `json:"categoryName,omitempty"`
	Source       string `json:"source"` // source_kind: bank_txn | scan | commerce_txn
	AmountCents  int64  `json:"amountCents"`
	VoucherID    int64  `json:"voucherId"`
}

Txn is one register row — a booked voucher projected to a single line.

type Vendor

type Vendor struct {
	Canonical       string   `json:"canonical"`
	Aliases         []string `json:"aliases,omitempty"`
	DefaultCategory string   `json:"defaultCategory,omitempty"` // COA account number
}

Vendor is one row of the vendor book: a canonical name, its alias spellings, and the COA expense account new bills from it default to.

type Voucher

type Voucher struct {
	SourceKind  string `json:"sourceKind"`
	SourceID    string `json:"sourceId"`
	PostingAt   string `json:"postingAt"`
	Description string `json:"description"`
	Legs        []Leg  `json:"legs"`
}

Voucher is one accounting EVENT: a set of legs that must balance, tagged with its idempotency key (SourceKind, SourceID) so the same source event posts exactly once.

Jump to

Keyboard shortcuts

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