Skip to Content

Finance Accounts

How financial accounts are stored, how balances are derived from transactions, and how clients call the accounts API.

Overview

An account is a per-user container for transactions (checking, savings, credit card, etc.). The server persists metadata and a starting initialBalance; the current balance is computed on read from active transactions, using persisted balance checkpoints so long histories do not require a full-table scan on every request. Accounts are soft-deleted (Status.DELETED) and excluded from list/get/total-balance queries.

Architecture

Clients call REST endpoints; AccountService delegates balance reads to AccountBalanceCheckpointService.

Balance algorithm

Checkpoints are stored per account at month-end and calendar year-end (period_end inclusive). Missing checkpoints are backfilled lazily on read for completed periods. Any transaction create/update/delete invalidates checkpoints with period_end >= the affected date.

Transaction typeEffect on balance
INCOME+amount
EXPENSE-amount
TRANSFER+amount (stored sign; paired transfer semantics are client/import responsibility)

Implementation: AccountBalanceCalculator, AccountBalanceMovementSum, AccountBalanceCheckpointService. Migration: V20__account_balance_checkpoints.sql.

Invalidation: changing initialBalance rebuilds all checkpoints for the account; soft-deleting an account removes its checkpoints. Clients can trigger the same full rebuild via POST /finance/accounts/{id}/rebuild-checkpoints (refresh icon on the account row in Finance → Accounts).

API

MethodPathRequestResponse
GET/finance/accountsAccountResponseDTO[]
GET/finance/accounts/total-balance{ "totalBalance": number }
GET/finance/accounts/{id}AccountResponseDTO or 404
POST/finance/accountsCreateAccountDTO (balance → stored as initialBalance)201 Created (no body)
PATCH/finance/accounts/{id}UpdateAccountDTO200 OK
DELETE/finance/accounts/{id}200 OK (soft-delete)
POST/finance/accounts/{id}/rebuild-checkpointsAccountResponseDTO (full checkpoint rebuild + fresh balance)

AccountResponseDTO: id, name, type, balance (computed), initialBalance, currency.

Route order: GET /total-balance is registered before GET /{id} so the literal path is not parsed as a UUID.

Client mapping

PlatformEntry point
Shared modelshared/.../models/finance/Account.kt
DesktopFinanceService.getAccounts() / getAccount()
Androidfinance_dataFinanceApi
Webweb-frontend/src/lib/api/finance.ts

Shared KMP Account includes isArchived for UI filtering; the server does not persist archive state (only soft-delete).

Known gaps

  • Account names are not enforced unique per user (documented in domain docs, not implemented).
  • POST / PATCH return status only; some clients expect a full Account body in the response.
  • FinanceService.addAccount / updateAccount expect a response body; server may not match until aligned.
  • Transfer balance semantics: calculator applies raw +amount for TRANSFER; net worth across accounts may require paired entries.

Testing