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
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type Account
- type AccountType
- type AskRequest
- type AskResponse
- type BalanceLine
- type BalanceSheet
- type BankQuestion
- type BankResult
- type BankTally
- type BankTxn
- type BankTxnRow
- type BookRequest
- type BookResponse
- type Connector
- type Direction
- type Extracted
- type Figure
- type FinancialPackage
- type GLRow
- type Importer
- type InboxItem
- type Leg
- type LineItem
- type Metrics
- type MetricsResponse
- type PartyType
- type PnL
- type PnLLine
- type Question
- type QuestionsResponse
- type Rule
- type ScanDraft
- type TellerLink
- type TrialBalance
- type TrialBalanceRow
- type Txn
- type VendorRow
- type Voucher
Constants ¶
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.
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 ¶
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.
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 ¶
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.
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 ¶
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.