Dosu LogoDosu Logo
Ask
Join our Discord
SurePublic
we-promise
DocumentsSure
Ledger Entry Accounting Model
Ledger Entry Accounting Model
Type
Topic
Status
Published
Created
Jul 10, 2026
Updated
Jul 10, 2026

Ledger Entry Accounting Model#

Sure uses a dual-entry accounting system built on three core models: Entry, Transaction, and Transfer. Every monetary event — income, expense, or movement between accounts — is stored as one or more Entry rows, each carrying a signed amount that encodes direction.


Amount Sign Convention#

The Entry#classification method encodes the rule directly :

  • Negative amount → "income" (inflow / credit)
  • Positive amount → "expense" (outflow / debit)

This convention is enforced at the controller layer: TransactionsController#entry_params converts the unsigned form value to a signed amount based on the hidden nature field — "inflow" maps to negative, "outflow" to positive .


Entry Structure#

Entry belongs to an account and delegates its domain type to one of three entryable types via Rails delegated_type:

Entryable typePurpose
TransactionIncome, expense, or transfer leg
ValuationPoint-in-time balance snapshot
TradeInvestment buy/sell event

Each Entry holds amount, currency, date, name, and optional transfer (via belongs_to :transfer) and parent_entry (for splits) associations .


How Transfers Pair Two Entries#

A Transfer is a join record linking exactly two Transaction rows: an outflow (positive amount) on the source account and an inflow (negative amount) on the destination account .

Transfer
 ├── outflow_transaction → Entry (amount > 0) on source account
 └── inflow_transaction → Entry (amount < 0) on destination account

The model enforces balance via transfer_has_opposite_amounts: the inflow must be negative, the outflow positive, and for same-currency transfers they must sum to zero .

Additional constraints :

  • Accounts must be different and belong to the same family
  • Entry dates must be within 4 days (pending) or 30 days (confirmed) of each other

Creating a Transfer: Transfer::Creator#

Transfer::Creator is the sole factory for new transfers. It accepts family, source_account_id, destination_account_id, date, amount, and optional exchange_rate, source_fee_amount, and destination_fee_amount.

Calling #create inside a single DB transaction:

  1. Builds the outflow transaction with amount (positive) on the source account
  2. Builds the inflow transaction with net_inflow * -1 (negative) on the destination account, currency-converting if needed
  3. Appends optional standard-kind fee transactions to either side
  4. Saves the Transfer, then calls sync_later on both accounts

Outflow kind Assignment#

The outflow transaction's kind controls how the transfer leg flows through budget and income-statement analytics. Transfer::Creator#outflow_transaction_kind picks the kind based on the destination account type:

Destination accountkind
loan?loan_payment
liability? (credit card, etc.)cc_payment
investment? or crypto? — and source is notinvestment_contribution
everything elsefunds_movement

The inflow transaction always receives kind: "funds_movement" regardless of account type .

When kind == "investment_contribution", the outflow is also auto-assigned the family's Investment Contributions category .

The class-level Transfer.kind_for_account mirrors this routing for display and matching use cases, but only accepts a single account — it lacks the !source_is_investment? guard present in Creator, which causes a known misclassification bug for investment-to-investment transfers .


Budget & Reporting Exclusion#

Two constants on Transaction determine analytics inclusion :

  • TRANSFER_KINDS = [funds_movement, cc_payment, loan_payment, investment_contribution] — identifies all transfer legs for search filter splits.
  • BUDGET_EXCLUDED_KINDS = [funds_movement, one_time, cc_payment] — dropped from budget/income-statement queries.

loan_payment and investment_contribution are intentionally absent from BUDGET_EXCLUDED_KINDS — they represent real cash outflow and appear as expenses in Reports.


Destroying a Transfer#

Transfer#destroy! resets both linked transactions' kind back to "standard" before destroying the join record, preventing orphaned transactions from retaining transfer-specific kinds. reject! additionally creates a RejectedTransfer record to suppress re-detection of the same pair.


Key Files#

FilePurpose
app/models/entry.rbCore ledger record; amount sign convention, split logic, classification
app/models/transfer.rbTransfer join record; validations, kind_for_account, destroy!, reject!
app/models/transfer/creator.rbService object for building paired dual entries
Documents
Account Authorization and Permissions
Account Balance Calculation
Account Creation
Account Lifecycle Management
Account Reporting Controls
Account Statement Management
Account Statement Reconciliation
Account Type Architecture
AI Bank Statement Extraction
AI Chat Interface
API Authentication and Authorization
Authentication and Session Management
Balance History System
Banking Data Encryption
Broker Activity Import
Budget Management
Category Management
Cryptocurrency Account Management
CSV Import and Column Mapping
Currency Management
Dashboard Filtering and Drilldowns
Depository Yield Modeling
Dev Container Setup
Dividend and DRIP Modeling
Docker Self-Hosting
Enable Banking Consent Management
Enable Banking Error Handling
Enable Banking OAuth and PSD2 Authentication
Family Data Export
Family Settings Management
Financial Insights and Metrics
Financial Reporting
FIRE Planning and Retirement Calculations
Goals and Savings Tracking
Internationalization and Localization
Investment Account Data Pipeline
Investment Account Flow Semantics
Investment Account Reconciliation
Investment Activity Labels
Investment Holdings Management
Investment Tax Treatment Classification
Investment Trade Conversion
Investment Trade Entry
Invitation Lifecycle and State Management
Ledger Entry Accounting Model
LLM Provider Configuration
LLM Request Timeout and Watchdog System
LLM Tool Calling
Manual Account Entry and Import
Manual Valuation
MCP Tool Access
Multi-Currency Exchange Rates
NDJSON Import System
Net Worth Balance Sheet
OIDC Provider Configuration
Pending Transaction Reconciliation
Plaid Integration
Provider Import Adapter
Rails Development Environment Configuration
Rails PWA Integration
Recurring Transactions and Cash Flow Projection
REST API Architecture
Securities Lookup
Security Exchange Identification
Security Price Import Pipeline
SimpleFIN Holdings Import
SimpleFIN Integration
SimpleFIN Liability Balance Normalization
SnapTrade Integration
Split Transactions
SSO Audit Logging
SSO Authentication Flow
SSO Provider Management
Timezone-Aware Financial Data Handling
Transaction Categorization
Transaction Deduplication
Transaction Exclusion
Transaction Filtering and Search
Transaction Management
Transaction Rule Engine
Transaction Sync Windows and Lookback
Transfer Management
Transfer Matching and Pairing
Turbo Frame Navigation
Yahoo Finance Integration
How split transaction child exclusion was implemented
Is it possible to edit the date of a synced transaction (e.g., one synced by LunchFlow)?
Provider Architecture