Finance Feature Guide
Comprehensive guide to the Finance module: accounts, transactions, budgets, savings goals, scheduled transactions, and the balance prediction engine.
Overview
The Finance module is the most complex feature in Oter. It provides full personal finance management — tracking income, expenses, and transfers across multiple accounts — plus a sophisticated balance prediction engine that projects future account balances using historical data, scheduled transactions, periodic spends, and configurable noise/scenarios. The module follows Clean Architecture on Android (three Gradle modules) and shares its HTTP client and models via the KMP shared layer.
Architecture
The finance feature spans five layers: Android presentation, Android domain, Android data, KMP shared services, and the Ktor server.
Data flows unidirectionally: UI intent → ViewModel → UseCase → Repository → DataSource → HTTP → Server.
ViewModel Structure
The presentation layer uses one ViewModel per domain concept, coordinated by a shared wrapper.
Each ViewModel is responsible for a single domain. FinanceActionsWrapper delegates actions to the right ViewModel. FinanceStateConverter merges all individual states into the single DesktopFinanceState consumed by the UI.
Data Models
Key Enums
TransactionType: INCOME, EXPENSE, TRANSFER
AccountType: CHECKING, SAVINGS, CREDIT_CARD, INVESTMENT, CASH, LOAN, OTHER
Category: 30+ values grouped as income (SALARY, INVESTMENT, FREELANCE, GIFT) and expense (FOOD, HOUSING, TRANSPORTATION, SHOPPING, HEALTH, SUBSCRIPTION, …)
CategoryVolatilityType (used by prediction engine):
| Type | Meaning | Example categories |
|---|---|---|
FIXED | Stable, predictable | Housing, Bills, Subscription, Debt |
SEMI_FIXED | Mostly stable with some variance | Groceries, Transportation, Health |
VARIABLE | Highly unpredictable | Entertainment, Shopping, Travel |
API Endpoints
Accounts
See accounts.md for balance derivation, soft-delete, client mapping, and tests.
| Method | Path | Description |
|---|---|---|
GET | /finance/accounts | List all accounts |
GET | /finance/accounts/total-balance | Sum balance across all accounts ({ "totalBalance": number }) — registered before /{id} |
GET | /finance/accounts/{id} | Get account by ID |
POST | /finance/accounts | Create account |
PATCH | /finance/accounts/{id} | Update account |
DELETE | /finance/accounts/{id} | Soft-delete account |
Transactions
| Method | Path | Description |
|---|---|---|
GET | /finance/transactions | List transactions (paginated, filterable) |
GET | /finance/transactions/{id} | Get transaction by ID |
POST | /finance/transactions | Create transaction |
PATCH | /finance/transactions/{id} | Update transaction |
DELETE | /finance/transactions/{id} | Delete transaction |
GET | /finance/transactions/byAccount/{accountId} | Transactions for an account |
POST | /finance/transactions/import | Import from text/CSV |
POST | /finance/transactions/import/preview | Preview import before confirming |
POST | /finance/transactions/balance-prediction | Run balance prediction |
POST | /finance/transactions/balance-prediction/affordability | What-if purchase affordability check |
GET | /finance/balance-scenarios | List saved balance scenarios |
POST | /finance/balance-scenarios | Create balance scenario |
PUT | /finance/balance-scenarios/{id} | Update balance scenario |
DELETE | /finance/balance-scenarios/{id} | Delete balance scenario (soft) |
Budgets
| Method | Path | Description |
|---|---|---|
GET | /finance/budgets | List budgets (paginated) |
GET | /finance/budgets/withProgress | Budgets with spending progress |
GET | /finance/budgets/byDateRange | Budgets in a date range |
GET | /finance/budgets/{id}/progress | Progress for a budget |
POST | /finance/budgets | Create budget |
PATCH | /finance/budgets/{id} | Update budget |
DELETE | /finance/budgets/{id} | Delete budget |
GET | /finance/budgets/{id}/transactions | Transactions counted against budget |
GET | /finance/budgets/unbudgeted/transactions | Transactions with no budget |
POST | /finance/budgets/unbudgeted/categorize | Auto-categorize unbudgeted |
POST | /finance/budgets/transactions/categorize | Auto-categorize all transactions |
Savings Goals
| Method | Path | Description |
|---|---|---|
GET | /finance/savings-goals | List goals |
GET | /finance/savings-goals/{id}/progress | Goal progress |
GET | /finance/savings-goals/{id}/remaining | Remaining amount |
POST | /finance/savings-goals | Create goal |
PATCH | /finance/savings-goals/{id} | Update goal |
DELETE | /finance/savings-goals/{id} | Delete goal |
POST | /finance/savings-goals/{id}/progress | Update goal progress |
Scheduled Transactions
| Method | Path | Description |
|---|---|---|
GET | /finance/scheduled-transactions | List (paginated) |
GET | /finance/scheduled-transactions/{id} | Get by ID |
POST | /finance/scheduled-transactions | Create |
PATCH | /finance/scheduled-transactions/{id} | Update |
DELETE | /finance/scheduled-transactions/{id} | Delete |
GET | /finance/scheduled-transactions/byAccount/{accountId} | By account |
Category Keywords & Analytics
| Method | Path | Description |
|---|---|---|
GET | /finance/category-keywords | List all keywords |
GET | /finance/category-keywords/by-category/{category} | Keywords for a category |
POST | /finance/category-keywords | Add keyword |
PATCH | /finance/category-keywords/{id} | Update keyword |
DELETE | /finance/category-keywords/{id} | Delete keyword |
GET | /finance/statistics | Financial statistics |
Balance Prediction Engine
The prediction engine is the core differentiator of the Finance module. It generates time-series balance projections across three scenarios.
Inputs and Scenarios
Each prediction point in the response represents one date and includes its balance, individual transactions, and per-scenario balances for that date.
Scenario lab (what-if)
Persisted balance scenarios and session inline adjustments layer on top of the core engine. See balance-prediction-scenario-lab.md for adjustment types, affordability verdicts, desktop UI, and platform coverage.
| Capability | Summary |
|---|---|
| Saved scenarios | CRUD via /finance/balance-scenarios; adjustments stored as JSON |
| Inline adjustments | NEW_TRANSACTION, MODIFY_CATEGORY, MODIFY_SCHEDULED in BalancePredictionFilters.inlineAdjustments |
| Baseline comparison | compareToBaseline returns baselinePoints without scenario/inline layer |
| Budget impact | Per-budget and unbudgeted bucket: baseline vs scenario spend + verdict |
| Affordability | POST .../affordability — one-time expense what-if with verdict precedence |
Desktop UI (composeApp/.../balanceprediction/): scenarios drawer, adjustment dialog, affordability dialog, budget impact on point details, baseline dashed line on chart. Category fields use CategoryDropdown (enum-backed).
Safety Features
| Feature | Description |
|---|---|
safetyFloor | Alert when projected balance drops below this threshold |
overdraftThreshold | Warn when balance goes negative (default 0.0) |
dangerEvents | List of dates/amounts where thresholds are breached |
safeToSpendToday | Maximum spendable today without triggering safety floor |
goalAnalysis | Probability of reaching a savings goal; required extra savings per week/month |
Pagination
Prediction results are paginated (default 100 points/page) to handle long date ranges efficiently. Clients specify page and pageSize in BalancePredictionFilters.
Client Behavior
For long date ranges, the UI may request additional pages by incrementing filters.page.
Key Files
| File | Purpose |
|---|---|
finance/finance_domain/.../repository/FinanceRepository.kt | Domain repository interface — the contract for all finance operations |
finance/finance_data/.../repository/FinanceRepositoryImpl.kt | Repository implementation, delegates to data source |
finance/finance_data/.../datasource/FinanceRemoteDataSource.kt | HTTP data source with null-safe response validation |
finance/finance_presentation/.../viewmodel/TransactionViewModel.kt | Transaction CRUD, filtering, import |
finance/finance_presentation/.../viewmodel/AccountViewModel.kt | Account management |
finance/finance_presentation/.../viewmodel/BudgetViewModel.kt | Budget tracking and auto-categorization |
finance/finance_presentation/.../viewmodel/BalancePredictionViewModel.kt | Prediction engine state, scenario selection |
finance/finance_presentation/.../viewmodel/FinanceActionsWrapper.kt | Unified actions interface delegating to focused ViewModels |
finance/finance_presentation/converter/FinanceStateConverter.kt | Merges all ViewModel states into DesktopFinanceState |
server/.../routing/FinanceRouting.kt | All 40+ HTTP endpoints with auth and pagination |
server/.../models/finance/FinanceDTOs.kt | Request/response DTOs |
shared/.../services/finance/FinanceService.kt | KMP Ktor HTTP client for all finance API calls |
shared/.../models/finance/BalancePrediction.kt | BalancePredictionFilters, BalancePredictionResponse, PeriodicSpend, DangerEvent |
shared/.../models/finance/ScenarioAdjustment.kt | ScenarioAdjustment, BalanceScenario, BudgetImpact, AffordabilityRequest / Result |
server/.../service/ScenarioAdjustmentMerger.kt | Merges scenarios + inline + periodic spends before engine |
server/.../service/BudgetImpactService.kt | Per-budget scenario vs baseline spend |
server/.../service/BalancePredictionAffordabilityService.kt | Affordability what-if endpoint |
shared/.../models/finance/Category.kt | Category enum + CategoryVolatilityType classification |
Testing
- Unit tests should cover
BalancePredictionViewModelscenario switching and state transitions. - Domain tests should cover use case edge cases: empty account list, zero balance, date range spanning month boundaries.
- Repository tests should verify
Result.failurepropagation when the HTTP layer returns an error. - Server tests should cover the prediction engine with mocked scheduled/historical transactions; verify danger events are emitted when
safetyFlooris breached.
Troubleshooting
Prediction returns empty points
- Verify
fromDate<toDateinBalancePredictionFilters. - Check that at least one
accountIdis valid and belongs to the authenticated user. - Confirm
pageSize> 0 andpage>= 0.
Budget progress not updating
- Ensure the transaction
categorymatches the budget’scategoryexactly. - Check
startDate/endDateoverlap between the transaction date and the budget period.
Transaction import assigns wrong categories
- Check
category-keywordsfor the relevant category — add or update keywords as needed. - Use the
previewendpoint before confirming an import to inspect auto-categorization results.
Scheduled transactions not appearing in prediction
- Verify
includeScheduledTransactions: trueinBalancePredictionFilters. - Confirm the scheduled transaction’s
startDatefalls within the prediction date range.
Related Docs
- Prediction engine — deep-dive into the balance prediction algorithm
- balance-prediction-scenario-lab.md — scenario lab, affordability, desktop UI
- Domain & models — cross-feature domain model reference
- Global architecture — how finance fits into the overall system architecture