books

package
v1.801.413 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: 26 Imported by: 0

Documentation

Overview

Package books is double-entry accounting: chart of accounts, ledger, bank reconciliation, and the reports that prove the books balance.

It serves /v1/books: a fixed chart of accounts, an append-only general ledger, bank feeds with reconciliation, receipt scanning, and the trial / P&L / position reports.

WHY THIS EXISTS. finance holds the money (a prepaid wallet: deposits + usage debits); billing PROJECTS that wallet for the customer UI. Neither keeps BOOKS — a general ledger with a chart of accounts, revenue recognition, and a trial 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.

IT RECORDS MONEY, IT NEVER MOVES IT. Three sources post: commerce GET /v1/billing/transactions (ingest.go), a read-only bank connector (bank.go), and a reviewed receipt scan (scan.go). Every one of them lands through the SAME post() choke point, and none of them can mint a deposit, credit, or payout — this domain 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 is the plain-language question about the org's books, e.g. "what is my
	// MRR?". Longer than 2000 characters is truncated, never refused.
	Question string `json:"question"`
	// From is the RFC3339 start of the metric window. Empty means all time, treated as a
	// single reporting period (see monthsBetween).
	From string `json:"from,omitempty"`
	// To is the RFC3339 end of the metric window. Empty means up to now.
	To string `json:"to,omitempty"`
}

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

type AskResponse

type AskResponse struct {
	// Answer is one or two sentences answering the question, every number in it taken
	// from Figures.
	Answer string `json:"answer"`
	// Figures are the grounded numbers the answer states, each already formatted.
	Figures []Figure `json:"figures"`
	// Followups are sharper questions to ask next, chosen from the same intent.
	Followups []string `json:"followups"`
	// Sources name the books reports the figures were computed from — "pnl",
	// "position", "trial".
	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 is the scanned document's file hash, as GET /v1/books/inbox and the scan
	// draft report it. It is the idempotency key: re-booking the same scan writes nothing.
	ScanID string `json:"scanId"`
	// Voucher is the reviewed voucher to post. Its source is FORCED to (scan, scanId)
	// server-side, so it can never be booked under another source's key.
	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 echoes the scan that was booked.
	ScanID string `json:"scanId"`
	// Posted is true when this call wrote the voucher, false when the same scan had
	// already booked and nothing was written.
	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 names the metric, e.g. "MRR" or "Runway".
	Label string `json:"label"`
	// Value is the figure already formatted through books' own money formatter, so a
	// consumer never re-derives it.
	Value string `json:"value"`
	// Period is the window the figure covers, e.g. "2026-07" or "all-time".
	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 is the chart-of-accounts number this side posts to, e.g. "5300".
	Account string `json:"account"`
	// Debit is the leg's debit in exact cents. Set this or Credit, not both.
	Debit int64 `json:"debit"`
	// Credit is the leg's credit in exact cents. Set this or Debit, not both.
	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 {
	// From is the RFC3339 start of the reporting window, exclusive; absent for all time.
	From string `json:"from,omitempty"`
	// To is the RFC3339 end of the reporting window, inclusive; absent for up to now.
	To string `json:"to,omitempty"`
	// Period is the human window label, e.g. "2026-07" or "all-time".
	Period string `json:"period"`
	// Months is the window length in whole months used to normalize MRR and burn.
	Months int `json:"months"`
	// MRR is monthly recurring revenue in cents.
	MRR int64 `json:"mrr"`
	// ARR is annualized recurring revenue in cents (MRR × 12).
	ARR int64 `json:"arr"`
	// Revenue is recognized revenue in cents over the period.
	Revenue int64 `json:"revenue"`
	// COGS is cost of goods sold in cents over the period.
	COGS int64 `json:"cogs"`
	// Burn is total expense in cents over the period.
	Burn int64 `json:"burn"`
	// GrossProfit is Revenue − COGS, in cents.
	GrossProfit int64 `json:"grossProfit"`
	// GrossMarginBps is GrossProfit / Revenue in basis points (7000 = 70%).
	GrossMarginBps int64 `json:"grossMarginBps"`
	// NetIncome is Revenue − Burn, in cents.
	NetIncome int64 `json:"netIncome"`
	// Cash is the bank + processor-clearing balance in cents as of To.
	Cash int64 `json:"cash"`
	// DeferredRevenue is the customer-wallet liability in cents as of To.
	DeferredRevenue int64 `json:"deferredRevenue"`
	// MonthlyBurn is net cash burned per month in cents; 0 when not losing cash.
	MonthlyBurn int64 `json:"monthlyBurn"`
	// RunwayMonths is Cash / MonthlyBurn; -1 means infinite (the org is not burning).
	RunwayMonths int64 `json:"runwayMonths"`
	// Figures is the same snapshot rendered through books' one money formatter.
	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.

The snapshot fields are Metrics' own, SPELLED OUT rather than embedded, because zip's schema walk publishes an embedded struct as a nested property while encoding/json flattens it — an Out that embedded Metrics would document a response no answer of this route matches. The copy cannot drift: metricsResponseOf carries every field, and TestMetricsResponseCarriesEveryMetricsField goes red on a Metrics field this struct or that constructor misses.

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 is the merchant substring the rule matches on, case-insensitively. It is
	// also the key an upsert writes by.
	Pattern string `json:"pattern"`
	// Category is the COA expense account a matching bill books to. An upsert normalizes
	// a slug ("cloud") to its account number.
	Category string `json:"category"` // COA account number
	// Priority breaks ties: when several patterns match, the highest wins.
	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 bank-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 VendorRow added in v1.801.350

type VendorRow struct {
	// Canonical is the vendor's one true name, and the key an upsert writes by.
	Canonical string `json:"canonical"`
	// Aliases are the other spellings a receipt may print the vendor under; a scan
	// matching any of them resolves to this vendor.
	Aliases []string `json:"aliases,omitempty"`
	// DefaultCategory is the COA expense account new bills from this vendor book to.
	// An upsert normalizes a slug ("software") to its account number.
	DefaultCategory string `json:"defaultCategory,omitempty"` // COA account number
}

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

It is …Row, like GLRow and BankTxnRow, because the OpenAPI schema namespace is FLAT across the whole fleet and admin already publishes a `Vendor` that is a different thing — a vendor COST LINE (vendor, service, amountCents, source). One name, two shapes is refused at the weave, and rightly: every generated SDK would bind whichever it read last. This side moved because this side's schema had never been published; a rename here costs no caller anything, and admin's would move a live SDK model.

type Voucher

type Voucher struct {
	// SourceKind is the idempotency namespace naming what booked this, e.g. "scan".
	SourceKind string `json:"sourceKind"`
	// SourceID is the source event's own id within that namespace. Together with
	// SourceKind it is the key that makes a repeat posting a no-op.
	SourceID string `json:"sourceId"`
	// PostingAt is the RFC3339 instant the event posts at — the time every statement
	// window filters on.
	PostingAt string `json:"postingAt"`
	// Description is the human line for the event, e.g. the vendor a bill came from.
	Description string `json:"description"`
	// Legs are the sides of the posting. They must balance: Σdebit == Σcredit, give or
	// take the 2¢ round-off allowance.
	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