package domain // Money is an exact decimal string bounded to signed 64-bit ten-thousandths. type Money string // Quantity is an exact decimal string bounded to signed 64-bit // hundred-millionths. It carries both share counts and unit prices, because // both exceed money's four places: a reinvested distribution settles a fraction // of a share, and a crypto unit price is quoted to six. type Quantity string // Account kinds. An empty kind is a cash account: the field was added after the // journal format, and absent means the original behaviour. const ( AccountCash = "cash" AccountInvestment = "investment" ) type Account struct { ID string `json:"id"` DisplayName string `json:"display_name"` Institution string `json:"institution"` Currency string `json:"currency"` // Kind is "cash" or "investment". An investment account also holds // positions, and its facts never reach the sign-based classification // fallback. Kind string `json:"kind,omitempty"` ExternalAccountID string `json:"external_account_id,omitempty"` IBAN string `json:"iban,omitempty"` // ReferenceIBAN is the counterpart this account settles cash against: a // broker exports no counterparty column, so deposits and withdrawals carry // this IBAN instead and pair with the funding account like any transfer. ReferenceIBAN string `json:"reference_iban,omitempty"` Active bool `json:"active"` } func (a Account) Investing() bool { return a.Kind == AccountInvestment } // Investment events. Cash events move money only; buy, sell and reinvest move // both money and position; corporate actions and position transfers move // position only and must never touch cash. const ( EventDeposit = "deposit" EventWithdrawal = "withdrawal" EventFee = "fee" EventInterest = "interest" // EventTaxSettlement is a broker settling withheld tax in cash, in either // direction: a loss-offset pot returning tax already paid, or a // recalculation charging more. EventTaxSettlement = "tax_settlement" EventDistribution = "distribution" EventBuy = "buy" EventSell = "sell" EventReinvest = "reinvest" EventCorporateAction = "corporate_action" EventPositionTransfer = "position_transfer" ) // Investment is the broker-native leg of an imported fact. Cash movement always // stays in Facts.Amount, so a position-only event has a zero amount; Gross, // Fee and Tax record the broker's own figures the amount was derived from. // // Quantity is signed: positive adds to the holding, negative removes it. The // export signs corporate actions and position transfers in its share column but // leaves buys and sells unsigned, so the sign is resolved at import, once. type Investment struct { Event string `json:"event"` InstrumentID string `json:"instrument_id,omitempty"` Quantity Quantity `json:"quantity,omitempty"` Price Quantity `json:"price,omitempty"` Gross Money `json:"gross,omitempty"` Fee Money `json:"fee,omitempty"` Tax Money `json:"tax,omitempty"` } // CashOnly reports an event that moves money without moving a position. func (i Investment) CashOnly() bool { switch i.Event { case EventDeposit, EventWithdrawal, EventFee, EventInterest, EventTaxSettlement, EventDistribution: return true } return false } // PositionOnly reports an event that moves a position without moving money. func (i Investment) PositionOnly() bool { return i.Event == EventCorporateAction || i.Event == EventPositionTransfer } // Settling reports an event that moves money and position together. func (i Investment) Settling() bool { switch i.Event { case EventBuy, EventSell, EventReinvest: return true } return false } // Instrument is a security held in an investment account, identified by ISIN. // The broker's description for one ISIN changes over time, so Name is editable // display text and never an identity. type Instrument struct { ID string `json:"id"` ISIN string `json:"isin"` Name string `json:"name"` Currency string `json:"currency"` // Symbol is the market listing this security is quoted under. One ISIN maps // to several listings in different currencies, and taking the wrong one // silently misstates wealth, so it is chosen once by hand and never // guessed. Without it the holding stays unpriced. Symbol string `json:"symbol,omitempty"` // Quote is the last known unit price and QuotedAt the day it is from, both // filled by the daily price job and hand-editable. A quote is a rate, not // money: a crypto unit price needs more than money's four places. Quote Quantity `json:"quote,omitempty"` QuotedAt string `json:"quoted_at,omitempty"` } type Facts struct { ID string `json:"id"` Source string `json:"source"` AccountID string `json:"account_id"` BookingDate string `json:"booking_date"` ValueDate string `json:"value_date,omitempty"` Amount Money `json:"amount"` Currency string `json:"currency"` RawDescription string `json:"raw_description"` ExternalID string `json:"external_id,omitempty"` Fingerprint string `json:"fingerprint"` Counterparty string `json:"counterparty,omitempty"` CounterpartyIBAN string `json:"counterparty_iban,omitempty"` // Investment is present exactly on facts imported from an investment // account. It is bank fact data and therefore immutable. Investment *Investment `json:"investment,omitempty"` } type Provenance struct { Source string `json:"source"` Model string `json:"model,omitempty"` Confidence string `json:"confidence,omitempty"` Timestamp string `json:"timestamp,omitempty"` Error string `json:"error,omitempty"` } type Enrichment struct { Kind string `json:"kind"` MerchantID string `json:"merchant_id,omitempty"` CategoryID string `json:"category_id,omitempty"` TagIDs []string `json:"tag_ids"` TransferPeerID string `json:"transfer_peer_id,omitempty"` Classification Provenance `json:"classification"` } type Transaction struct { Facts Facts `json:"facts"` Enrichment Enrichment `json:"enrichment"` } type Category struct { ID string `json:"id"` Name string `json:"name"` ParentID string `json:"parent_id,omitempty"` Kind string `json:"kind"` Hint string `json:"hint,omitempty"` } type Tag struct { ID string `json:"id"` Name string `json:"name"` Hint string `json:"hint,omitempty"` } type Merchant struct { ID string `json:"id"` Name string `json:"name"` Aliases []string `json:"aliases"` DefaultCategoryID string `json:"default_category_id,omitempty"` DefaultTagIDs []string `json:"default_tag_ids"` UseDefaults bool `json:"use_defaults"` } type Dataset struct { Accounts []Account `json:"accounts"` Categories []Category `json:"categories"` Tags []Tag `json:"tags"` Merchants []Merchant `json:"merchants"` Instruments []Instrument `json:"instruments"` Transactions []Transaction `json:"transactions"` } const ExpenseFallback = "cat_expenses_unclassified" const IncomeFallback = "cat_income_unclassified" // KindInvestment is the enrichment kind for broker facts. Like a transfer it // carries no category or merchant and never reaches spending analytics: money // moving between your own cash and your own positions is not income or // spending, and the AI must never see it. const KindInvestment = "investment"