// Package model holds the core domain types shared by every other package. package model import ( "fmt" "strings" ) // Account is one folder in the data root. type Account struct { ID int64 Slug string // folder name, unique Name string // human readable, from account.toml Currency string MinorDigits int } // Transaction is a single line extracted from a statement, plus whatever the // rules engine and the user have said about it. // // Amount is stored in minor units (cents) and signed: negative is an outflow. type Transaction struct { ID int64 AccountID int64 AccountSlug string Currency string MinorDigits int Fingerprint string Date string // YYYY-MM-DD Description string AmountMinor int64 SourceFileID int64 SourcePath string // Type is the bank's own classification, e.g. Revolut's CARD_PAYMENT. Type string // BalanceMinor is the running balance the statement reported after this // transaction, when it reports one. Used to verify the balance chain. BalanceMinor *int64 // RuleTag is the category, decided entirely by rules.toml. It is derived // state: `money retag` rewrites it wholesale. RuleTag string // TransferID names the matched movement this transaction is one leg of, or // nil when it is not part of one. Derived from the [[transfer]] blocks in // rules.toml and rewritten wholesale alongside the tags. TransferID *int64 } // IsTransferLeg reports whether this transaction was paired with its opposite // number on another account, and so is money moved rather than money spent. func (t Transaction) IsTransferLeg() bool { return t.TransferID != nil } // TransferTag is shown in the tag column for a matched transfer leg that no // rule has tagged, so a paired leg reads as accounted for rather than as a // blank waiting to be filled in. It is bracketed like report.Untagged because // it is the same kind of thing: a label the tool supplies, not one the user // wrote. const TransferTag = "(transfer)" // DisplayTag is the tag to show for a transaction. // // It is display only. Nothing writes it back: rule_tag stays exactly what // rules.toml made it, so a transfer can never smuggle a tag into the index and // `money retag` remains safe to run at any time. A rule tag wins when there is // one, since that is the user's own word for the transaction and the transfers // screen is where the pairing is explained anyway. func (t Transaction) DisplayTag() string { if t.RuleTag != "" { return t.RuleTag } if t.IsTransferLeg() { return TransferTag } return "" } // FormatAmount renders the amount using the account's minor-unit scale. func (t Transaction) FormatAmount() string { return FormatMinor(t.AmountMinor, t.MinorDigits) } // FormatMinor renders minor units as a decimal string, e.g. -4520/2 -> "-45.20". func FormatMinor(minor int64, digits int) string { if digits <= 0 { return fmt.Sprintf("%d", minor) } neg := minor < 0 if neg { minor = -minor } scale := int64(1) for i := 0; i < digits; i++ { scale *= 10 } whole, frac := minor/scale, minor%scale s := fmt.Sprintf("%d.%0*d", whole, digits, frac) if neg { s = "-" + s } return s } // NormalizeDescription collapses whitespace and upper-cases, so that // fingerprints and glob matching are insensitive to statement formatting noise. func NormalizeDescription(s string) string { return strings.ToUpper(strings.Join(strings.Fields(s), " ")) }