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 type | Effect 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
| Method | Path | Request | Response |
|---|---|---|---|
GET | /finance/accounts | — | AccountResponseDTO[] |
GET | /finance/accounts/total-balance | — | { "totalBalance": number } |
GET | /finance/accounts/{id} | — | AccountResponseDTO or 404 |
POST | /finance/accounts | CreateAccountDTO (balance → stored as initialBalance) | 201 Created (no body) |
PATCH | /finance/accounts/{id} | UpdateAccountDTO | 200 OK |
DELETE | /finance/accounts/{id} | — | 200 OK (soft-delete) |
POST | /finance/accounts/{id}/rebuild-checkpoints | — | AccountResponseDTO (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
| Platform | Entry point |
|---|---|
| Shared model | shared/.../models/finance/Account.kt |
| Desktop | FinanceService.getAccounts() / getAccount() |
| Android | finance_data → FinanceApi |
| Web | web-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/PATCHreturn status only; some clients expect a fullAccountbody in the response.FinanceService.addAccount/updateAccountexpect a response body; server may not match until aligned.- Transfer balance semantics: calculator applies raw
+amountforTRANSFER; net worth across accounts may require paired entries.
Testing
- Unit:
AccountBalanceCalculatorTest - Checkpoints (DB):
AccountBalanceCheckpointServiceTest - DB integration:
AccountDBTest(requires Postgres test DB onlocalhost:5431) - Repository wiring:
AccountRepositoryTest