## Summary Wave 2 structural refactor: the same financial math was copy-pasted across the dashboard and the analytics API endpoints, so it could silently diverge between screens — a real risk in a finance app. This consolidates the duplicated calculations into single sources of truth, kills a net-worth N+1, and aligns the PHP/TS rule engines and the `TransactionSource` enum. Every change is **behavior-preserving**; the numeric outputs of every endpoint are unchanged and are locked down with characterization/parity tests. Builds on merged #640 (Wave 1); no file overlap. No dependencies changed. ## Changes (per commit) - **Consolidate savings-rate math into `CashflowSummaryService`** — `savings_rate` and `net` were byte-identical inline in `DashboardController` and `Api/CashflowAnalyticsController`. Extracted to `CashflowSummaryService::summarize(income, expense)`; both controllers now spread its result (same keys, same values). - **Move income/expense-side classification onto the `Transaction` model** — the income/expense side test was reimplemented in three places (`Api/TransactionAnalysisController`, `Api/CashflowAnalyticsController`, `DashboardController`). Now `Transaction::isIncomeSide()` / `isExpenseSide()`. - **Extract duplicated `getCategorySpending` into `CategorySpendingService`** — the tree-rollup expense-spending query was duplicated verbatim between `DashboardController` and `Api/DashboardAnalyticsController`. Moved to `CategorySpendingService::forPeriod()` (drill-parent parameterized). - **Batch net-worth balance lookups to kill the per-account N+1** — `Api/DashboardAnalyticsController::calculateNetWorthAt` ran one `AccountBalance` query per account per compared period. Now uses the existing `BalanceLookup::forAccounts()` (fixed 3 queries via carry-forward seed + in-range records), reproducing the exact "latest balance <= date, else 0" semantics. - **Align server rule normalization with the client and lock it with parity fixtures** — `AutomationRuleService::normalizeRuleJson` protected only `['description','notes']` while `rule-engine.ts` also protected `creditor_name`/`debtor_name`. Aligned to the superset (structurally a no-op since those var names are already lowercase, so no matching change) and added shared PHP+TS parity fixtures so the two engines can never drift unnoticed. - **Add missing `TransactionSource` cases to the TS type** — `transaction.ts` was missing `enablebanking`/`wise`; now mirrors `App\Enums\TransactionSource`. - **Guard `sumTransactions` against unsupported category types** (reviewer fix) — replaced the income/expense ternary that silently treated any non-Income type as expense with a `match` that throws on Savings/Investment/Transfer. - **Document category eager-load expectation on `Transaction` side methods** (reviewer fix) — doc-only note to prevent a future N+1. ## Test plan New tests: - `tests/Unit/Services/CashflowSummaryServiceTest.php` — net/savings-rate/rounding/div-by-zero. - `tests/Feature/TransactionSideClassificationTest.php` — income/expense side across signs, uncategorized, and transfer/savings/investment = neither side. - `tests/Feature/DashboardAnalyticsTest.php` — new net-worth test asserts both the values (600000 / 540000) and a flat balance-query count (<= 3) regardless of account count. - `tests/Feature/RuleEngineParityTest.php` + `resources/js/lib/rule-engine-parity.test.ts` + `tests/Fixtures/rule-engine-parity.json` — one shared fixture set driving both the PHP and TS rule engines. Results (targeted, local): - `--filter=Cashflow` (exclude Browser): 57/57 passed - `--filter=DashboardAnalytics`: 41/41 passed - `--filter=AutomationRule`: 60/60 passed - `--filter=TransactionSideClassification|CashflowSummaryService|RuleEngineParity`: 21/21 passed - `bun run test rule-engine` (vitest): 13/13 passed - `vendor/bin/pint --test`: pass; `bun run lint`: 0 errors; `bun run format:check`: clean - `bun run types`: 157 errors (unchanged pre-existing baseline), 0 in touched files Note: the 6 `Cashflow*` Browser tests fail locally only on "Vite manifest not found" (no build present); they are environmental, not logic, and pass in CI. ## Reviewer findings Two read-only reviewers (architecture/quality and product/behavior) reviewed the diff. **Addressed** - Both flagged that `sumTransactions` silently treated any non-Income type as expense — added a throwing `match` guard. - Eager-load expectation documented on the `Transaction` side methods. **Verified identical** (behavior reviewer): income/expense/net/savings_rate across all three endpoints; net worth for both compared dates including no-record / all-records-after-range / same-date edge cases; category spending (uncategorized excluded, soft-deleted categories excluded, rollup/drill preserved); rule-engine normalization output; multi-currency conversion. **Deferred (documented)** - The income/expense **summation** itself is still computed three ways with differing uncategorized-transaction handling (dashboard `whereExists` + sign vs analytics `join` excluding uncategorized vs in-memory `isIncomeSide`). Unifying it would change numbers, so it is out of scope for this behavior-preserving PR — worth a dedicated follow-up. - `savings_rate` keeps its `int|float` union (int `0` when income is 0). Intentionally preserved to keep JSON output byte-identical. - `BalanceLookup`'s `empty()` guard never short-circuits a `Collection`, so a zero-account user runs 3 empty (harmless) queries. Left untouched — it lives in a shared, unchanged service and only affects a no-account edge case. Do not merge before Wave 1 (#640) is in main. |
||
|---|---|---|
| .agents/skills | ||
| .claude | ||
| .cursor | ||
| .github | ||
| .opencode/skills | ||
| .pi | ||
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| docs | ||
| experiments | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| screenshots | ||
| scripts | ||
| src/lib/crypto | ||
| storage | ||
| templates/coolify | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .env.production.example | ||
| .gitattributes | ||
| .gitignore | ||
| .mcp.json | ||
| .php-cs-fixer.dist.php | ||
| .php-version | ||
| .prettierignore | ||
| .prettierrc | ||
| .release-it.json | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| Dockerfile.production | ||
| LICENSE.md | ||
| LOCALIZATION.md | ||
| ONBOARDING.md | ||
| README.md | ||
| artisan | ||
| autoresearch-dashboard.md | ||
| autoresearch.jsonl | ||
| autoresearch.md | ||
| autoresearch.sh | ||
| boost.json | ||
| bun.lock | ||
| components.json | ||
| compose.yaml | ||
| composer.json | ||
| composer.lock | ||
| docker-compose.production.yml | ||
| eslint.config.js | ||
| falcode.json | ||
| opencode.json | ||
| package-lock.json | ||
| package.json | ||
| phpstan-baseline.neon | ||
| phpstan.neon | ||
| phpunit.xml | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
| whispermoney | ||
| worktree.sh | ||
README.md
Deutsch | Español | français | 日本語 | 한국어 | Português | Русский | 中文
Whisper Money
The most secure way to understand your finances.
Whisper Money is a privacy-first personal finance application that helps you track, categorize, and understand your spending—all while keeping your financial data encrypted and secure.
🎮 Try the Demo: Experience Whisper Money with our demo account - no registration required!
💬 Join our Community: Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our Discord server! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.
Features
- 🔐 Privacy-first — Your data is never shared with third parties. You own it
- 🏦 Bank account management — Track multiple accounts in one place
- 📊 Transaction categorization — Automatic and manual categorization
- 🤖 Automation rules — Set up rules to auto-categorize transactions
- 📈 Financial insights — Understand your spending patterns
Tech Stack
- Backend: Laravel 12, PHP 8.4
- Frontend: React 19, Inertia.js v2, TypeScript
- Styling: Tailwind CSS v4
- Database: MySQL
- Cache/Queue: Redis
- Testing: Pest v4
Running Locally
Quick Start (Recommended)
The easiest way to get started is using our automated setup script:
bash <(curl -fsSL https://whisper.money/setup.sh)
After installation, just visit https://whisper.money.localhost in your browser.
Manual Setup
If you prefer to set up manually:
- Clone the repository:
git clone https://github.com/whisper-money/whisper-money.git
cd whisper-money
- Run the setup script:
whispermoney install
Available Commands
Important: You must run
whispermoney installbefore using any other command. If you skip the install step, commands likestartwill not work.
Once installed, you can use the whispermoney command for common tasks:
# Start all services
whispermoney start
# Stop all services
whispermoney stop
# Upgrade to latest version
whispermoney upgrade
# Interactive menu
whispermoney
Development Server
For active development with hot reloading:
composer run dev
This will concurrently start:
- PHP development server (via Portless HTTPS proxy)
- Queue worker
- Log viewer (Pail)
- Vite dev server
The application will be available at https://dev.whisper.money.localhost. In git worktrees, the branch name is automatically prepended (e.g. https://fix-ui.dev.whisper.money.localhost).
Running with Docker (Production Image)
For testing the production Docker image locally:
- Copy the production environment file:
cp .env.production.example .env
- Start the services:
docker compose -f docker-compose.production.yml up -d
The application will be available at http://localhost:8080.
To use a different port, set APP_PORT:
APP_PORT=3000 docker compose -f docker-compose.production.yml up -d
Deploying to Coolify
Whisper Money can be easily deployed to Coolify using our Docker Compose template.
Quick Deploy
- In Coolify, create a new resource and select Docker Compose
- Choose Empty Compose File as the source
- Paste the contents from our template: 👉 whisper-money.yaml
- Deploy!
The template includes:
- Whisper Money application container
- MySQL 8.0 database with health checks
- Persistent volumes for data and storage
- Auto-generated database credentials
Required Environment Variables
| Variable | Description |
|---|---|
RESEND_API_KEY |
Email service API key (for password resets, notifications) |
Note:
APP_KEYandAPP_URLare auto-configured. The container generates anAPP_KEYon first startup if not provided.
Optional Environment Variables
| Variable | Default | Description |
|---|---|---|
DRIP_EMAILS_ENABLED |
true |
Enable drip emails (welcome, onboarding, feedback) |
HIDE_AUTH_BUTTONS |
false |
Hide login/register buttons on landing page |
SUBSCRIPTIONS_ENABLED |
false |
Enable Stripe subscriptions |
STRIPE_KEY |
- | Stripe publishable key |
STRIPE_SECRET |
- | Stripe secret key |
STRIPE_WEBHOOK_SECRET |
- | Stripe webhook signing secret |
Star History
License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.