Files
finance-duck/internal/domain/model.go
T

217 lines
8.7 KiB
Go

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"`
// AnchorBalance is the bank's booked (CLBD) balance on AnchorDate, captured
// once from open banking after a sync. It fixes the start balance of a
// date-windowed history: the money that existed before the recorded rows is
// AnchorBalance less every movement booked through AnchorDate, so the
// account's real balance is computable without complete history. The bank's
// figure is stored verbatim — the start balance is derived, never stored —
// so importing older history later corrects the derivation by itself.
// Cash accounts only: a broker export carries its complete history.
AnchorBalance Money `json:"anchor_balance,omitempty"`
AnchorDate string `json:"anchor_date,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"`
}
// Asset is a possession valued by hand: a house, a car, anything without a
// market feed. Value is what the owner states it is worth and ValuedAt the day
// that estimate was made, so a stale figure is visible rather than silently
// trusted. A negative value records a liability such as a mortgage.
type Asset struct {
ID string `json:"id"`
Name string `json:"name"`
// Kind is free display text grouping the asset: "Real estate", "Vehicle".
Kind string `json:"kind,omitempty"`
Currency string `json:"currency"`
Value Money `json:"value"`
ValuedAt string `json:"valued_at"`
}
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"`
Assets []Asset `json:"assets"`
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"