Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
b9ea3dd
Make investment transfer pairs reciprocal and atomic
31b4 Oct 16, 2026
679dcec
Set up investment epic integration workflow
31b4 Oct 17, 2026
c23b814
Merge remote-tracking branch 'origin/epic/123-investment-rework' into…
31b4 Oct 17, 2026
7d61f64
Clarify investment sub-issue branch naming
31b4 Oct 17, 2026
c6a8a45
Merge remote-tracking branch 'origin/epic/123-investment-rework' into…
31b4 Oct 17, 2026
ce12e97
Merge pull request #125 from apptrackit/task/122-investment-transfer-…
31b4 Oct 17, 2026
3464322
Define investment accounting and legacy compatibility contract
31b4 Oct 17, 2026
42b698a
Merge pull request #136 from apptrackit/task/126-investment-accountin…
31b4 Oct 17, 2026
9eeb646
Add explicit investment holding metadata and opening positions
31b4 Oct 17, 2026
dff70e5
Merge pull request #137 from apptrackit/feature/127-investment-holdin…
31b4 Oct 17, 2026
3585a53
Record atomic cash-linked investment buys and sells
31b4 Oct 17, 2026
522a9de
Keep investment migration notes in the unreleased section
31b4 Oct 17, 2026
7549721
Merge pull request #139 from apptrackit/feature/128-direct-investment…
31b4 Oct 17, 2026
23bd8f7
Track dated manual valuations and whole-asset ownership
31b4 Oct 17, 2026
f474146
Keep zero-value asset ownership and opening cost explicit
31b4 Oct 17, 2026
7e00034
Merge pull request #140 from apptrackit/feature/129-manual-investment…
31b4 Oct 17, 2026
8c1f044
Normalize and cache investment quotes, history and dated FX
31b4 Oct 17, 2026
367ca98
Merge pull request #141 from apptrackit/feature/130-market-data-history
31b4 Oct 17, 2026
c3db1ec
Unify investment values, weighted-average cost and trade previews
31b4 Oct 17, 2026
9ab803c
Distinguish native execution basis from reporting cash invested
31b4 Oct 17, 2026
2cf425c
Merge pull request #142 from apptrackit/feature/131-investment-metrics
31b4 Oct 18, 2026
0ceb7c4
Build dated portfolio history and estimated range returns
31b4 Oct 18, 2026
d1f814e
Preserve corporate-action coverage before selected history ranges
31b4 Oct 18, 2026
bf61664
Handle historical midnight daylight-saving transitions
31b4 Oct 18, 2026
ee41ea9
Merge pull request #143 from apptrackit/feature/132-investment-portfo…
31b4 Oct 18, 2026
fd67ce4
Build responsive investment overview with native holding trends
31b4 Oct 18, 2026
7b735f5
Restore browser storage stub before each investment navigation test
31b4 Oct 18, 2026
de2b72d
Merge pull request #144 from apptrackit/feature/133-investment-overview
31b4 Oct 18, 2026
9d0aace
Build holding panels and guarded direct-cash trade editor
31b4 Oct 18, 2026
71f952a
Preserve original trade requests through uncertain save recovery
31b4 Oct 18, 2026
792fb9b
Merge pull request #145 from apptrackit/feature/134-investment-detail…
31b4 Oct 18, 2026
300f463
Validate investment rework and make shared cash writes atomic
31b4 Oct 18, 2026
8d019fd
Merge pull request #146 from apptrackit/task/135-investment-final-val…
31b4 Oct 18, 2026
c01ee92
Fix decimal balance drift with guarded ledger arithmetic
31b4 Oct 18, 2026
8c48307
Let server preview validate legacy holding residue
31b4 Oct 18, 2026
eecb4ca
Merge pull request #149 from apptrackit/bug/148-decimal-balance-drift
31b4 Oct 19, 2026
c554ef9
Use recorded trades as investment position history
31b4 Oct 19, 2026
e69d12f
Confirm legacy investment payment details without balance changes
31b4 Oct 19, 2026
d310106
Distinguish historical FX gaps from missing purchase details
31b4 Oct 19, 2026
a9b8abf
Merge pull request #151 from apptrackit/bug/150-recorded-investment-h…
31b4 Oct 19, 2026
7755c65
Isolate market requests and retain valid historical FX series
31b4 Oct 19, 2026
d3e368d
Merge pull request #160 from apptrackit/bug/159-market-request-isolation
31b4 Oct 19, 2026
ade1a0c
Exclude dividends from investment price returns and purchase review
31b4 Oct 19, 2026
048ff5c
Present modest quote delays without actionable stale-price warnings
31b4 Oct 19, 2026
411386e
Merge pull request #164 from apptrackit/bug/163-delayed-quote-present…
31b4 Oct 19, 2026
06c72c6
Add EUR investment reporting across portfolio history and previews
31b4 Oct 19, 2026
a50a936
Align holdings rows with trailing controls and supported return perce…
31b4 Oct 19, 2026
808ac75
Merge pull request #168 from apptrackit/feature/167-holdings-presenta…
31b4 Oct 19, 2026
2e6ad01
Merge branch 'epic/123-investment-rework' into feature/165-investment…
31b4 Oct 19, 2026
d688c07
Merge pull request #166 from apptrackit/feature/165-investment-eur-view
31b4 Oct 19, 2026
bc47cfc
Merge branch 'epic/123-investment-rework' into bug/161-dividend-exclu…
31b4 Oct 19, 2026
0d4be73
Merge pull request #162 from apptrackit/bug/161-dividend-excluding-re…
31b4 Oct 19, 2026
ba0004f
Merge main into investment epic and preserve guarded split cash writes
31b4 Oct 19, 2026
84e2cf8
Account for nullable split membership in the combined schema regression
31b4 Oct 19, 2026
6c79af6
Merge pull request #170 from apptrackit/task/169-sync-main-investment…
31b4 Oct 19, 2026
ffdd531
Show gain on net invested in portfolio summary
31b4 Oct 19, 2026
c850eae
Remove investment explanatory copy requested in review
31b4 Oct 19, 2026
904fb35
Move allocation below portfolio chart and summary
31b4 Oct 19, 2026
ecf8b30
Emphasize total gain in portfolio history tooltip
31b4 Oct 19, 2026
819f92c
Compact investment overview into balanced responsive sections
31b4 Oct 19, 2026
2e5d422
Remove duplicate investment privacy button
31b4 Oct 19, 2026
426dcdc
Center holding details and show all dated trade markers
31b4 Oct 19, 2026
193d754
Expose historical review for versioned linked legacy trades
31b4 Oct 19, 2026
0e19d6b
Prefill legacy trade settlement from verified linked cash
31b4 Oct 19, 2026
5812242
Use the shared buy sell editor for investment transfers
31b4 Oct 19, 2026
ce27ae8
Give holding details a clear value gain and position hierarchy
31b4 Oct 20, 2026
790e219
Display split-adjusted holding charts and aligned trade markers
31b4 Oct 20, 2026
d7ac6ed
Show gain breakdown as visible zero centered comparison cards
31b4 Oct 20, 2026
b534893
Implement holding detail design 2a with compact chart first layout
31b4 Oct 20, 2026
99825de
Keep holding chart caption height stable on hover
31b4 Oct 20, 2026
a7a280d
Revert "Keep holding chart caption height stable on hover"
31b4 Oct 20, 2026
be0a485
Reapply "Keep holding chart caption height stable on hover"
31b4 Oct 20, 2026
46bf542
Open holding details from the whole row
31b4 Oct 20, 2026
ba6b323
Restrict Dashboard transfer creation to cash accounts
31b4 Oct 20, 2026
ac00fd9
Place settlement confirmation after the trade review
31b4 Oct 20, 2026
8d27a0d
Show asset selection before investment identity fields
31b4 Oct 20, 2026
59f93bb
Merge pull request #172 from apptrackit/feature/171-investment-ui-ux-…
31b4 Oct 20, 2026
42b818c
Summarize investment rework in a dedicated changelog section
31b4 Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
pull_request:
branches: [main]
branches: [main, 'epic/**']
push:
branches: [main]
branches: [main, 'epic/**']
merge_group:
workflow_dispatch:

Expand Down
23 changes: 19 additions & 4 deletions AGENTS.md

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ Intermediate package/UI version bumps are grouped under the next published GitHu

## Unreleased

### Investment rework

- **Redesigned portfolio overview.** A responsive Investments page brings together portfolio value, net invested, gains, allocation and sortable holdings with 30-day trends. Filters, privacy controls and clear loading/retry states work across desktop and mobile. [#123](https://github.com/apptrackit/finance/issues/123)
- **Holding details at a glance.** Open any holding for its value, gain, position, price chart and trade history in a compact dialog. Split-adjusted charts keep historical prices and trade markers comparable across stock splits.
- **Direct cash-linked Buy/Sell.** A dedicated trade editor records purchases and sales with actual execution prices, fees and settlement amounts, previewing cash and position changes before saving. Editing or deleting a linked trade updates both cash and holdings together; new trades start from Investments.
- **Clearer investment performance.** Track remaining cost basis, net invested, realized and unrealized gains, and covered daily/range return estimates. Price returns exclude dividends; missing historical costs or exchange rates stay explicit, with a review flow for completing old purchase details.
- **Historical portfolio tracking.** Compare portfolio value with net invested over selectable calendar ranges, using dated holdings, prices, valuations and exchange rates. History includes sold and archived holdings during their ownership periods, with visible gaps where records are incomplete.
- **Flexible holdings and manual valuations.** Set up ETFs, stocks, crypto and manual assets with separate pricing and unit choices. Record dated opening positions, quantity corrections and total/per-unit appraisals; valuation updates keep their own history and reminders without creating cash movements.
- **HUF, USD and EUR reporting.** Switch reporting currency across portfolio summaries, history, holdings and trade previews. Market prices retain their native currency and source time, with delayed-price indicators, stale-value warnings and dated exchange rates for historical values and purchase costs.

### Added

- One transaction composer for single, split and adjustment entries. **Split into categories** divides one payment over several category lines (shared account, date and description; the first line takes the rest of the total), saved atomically and shown as one expandable row. **Save and add another** keeps type, account and date for the next payment. Recording a balance difference opens the same controls with the amount and account fixed. New `transaction_splits` table and `transactions.split_id` (migration 017), `/transactions/splits` endpoints, and `transaction_splits` in the JSON export. [#138](https://github.com/apptrackit/finance/issues/138)
Expand All @@ -15,13 +25,19 @@ Intermediate package/UI version bumps are grouped under the next published GitHu
### Changed

- Bulk add and the separate balance-adjustment choice and split dialogs are replaced by the composer. Per-row account and date, sliders, auto-balance and fill-with-total are gone; log unrelated payments with Save and add another.

- Dashboard uses a shared period picker, a responsive balance/activity summary, compact transaction filters, and a combined review queue with explicit confirmation actions. Transaction rows show neutral transfers, daily cash-flow totals, and native/converted amounts; privacy masking and loading states follow the new layout.

### Fixed

- Cash and investment balance changes use decimal arithmetic to prevent floating-point residue while preserving genuine fractional amounts. Payments, transfers, adjustments, pending confirmations and recurring execution update balances and audit records together, with guards against duplicate or conflicting changes.
- Dashboard net worth respects investment holding exclusions; Investments continues to show all active holdings.
- Buttons, tabs, switches, menus, selects, checkboxes and other enabled click targets show a pointer cursor again across the app; Tailwind CSS 4 had reset buttons to the default arrow. Disabled controls keep the default cursor. [#157](https://github.com/apptrackit/finance/issues/157)

### Migration and upgrade notes

- Apply all pending migrations before deploying the updated API, MCP and client. Keep both `017-investment-transfer-links.sql` and `017-transaction-splits.sql` under their original names, followed by migrations 018–021. Migration history tracks the full migration name, so upgrades apply only missing files; see the [migration guide](api/migrations/README.md).
- The investment migrations add holding metadata, linked trade facts, manual valuation history and a reconstructible market-data cache. Existing financial records are preserved; unknown historical purchase details are not fabricated. The cache is excluded from financial exports. See the [holding](docs/investment-holdings.md), [trade](docs/investment-trades.md) and [manual valuation](docs/manual-investment-valuations.md) guides for compatibility details.

## v3.3 — 2026-10-15

[GitHub release](https://github.com/apptrackit/finance/releases/tag/v3.3) · [Compare v3.2…v3.3](https://github.com/apptrackit/finance/compare/v3.2...v3.3)
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The latest published release is [v3.3](https://github.com/apptrackit/finance/rel
| Transactions | Income and expenses, categories and icons, linked transfers, search and date filters, split payments, save-and-add-another entry, balance adjustments, and recent-change indicators. |
| Upcoming transactions | One-time future income and expenses, projected cash balances, and explicit confirmation or decline without changing the posted balance early. |
| Recurring schedules | Daily, weekly, monthly, and yearly options; transaction and transfer schedules; pause/resume, end dates, occurrence limits, and a calendar view. |
| Investments | Stock/crypto symbol search, Yahoo Finance quotes and price history, buy/sell records, manual assets, allocation, cost basis, and gain/loss views. |
| Investments | Responsive portfolio history, allocation and sortable ETF/stock/crypto/manual holdings with native trends, coverage alerts, normalized free sources, dated manual values and exact linked cash trades. |
| Analytics | Configurable widgets for cash trends, AI financial forecasts, income/expense trends and breakdowns, individual account trends, Money Map, and top expenses. |
| AI financial forecasts | Saved HUF forecasts with 7/30/90-day ranges, daily cash paths, report history, source-data freshness, and privacy-aware narratives. |
| MCP review | Prepare income, expense, or cash transfer drafts in conversation; list and correct ordinary drafts or transfer pairs; review and confirm or decline them in the app. |
Expand All @@ -52,17 +52,27 @@ Cash income is positive and expenses are negative. Posted transactions affect ac

Future-dated ordinary transactions remain pending until confirmed. Confirmation is guarded against duplicate balance updates and cannot post a future-dated transaction. The client sends its local calendar date in `X-Client-Date` so the API and browser agree on what is due.

Transfers link both sides of a movement and are excluded from income/expense aggregates. Cash-to-investment transfers link the cash transaction to a buy/sell record in `investment_transactions`.
Transfers link both sides of a movement and are excluded from income/expense aggregates. Cash-linked investment purchases and sales store reciprocal `linked_transaction_id` values on the cash transaction and its buy/sell record in `investment_transactions`. Standalone investment records keep this field null. Linked trade creation, edits, and deletion update both records, native cash/holding balances, and audit entries in one guarded D1 batch. Edit the pair through its cash transaction; deleting either record removes the pair and reverses both movements once.

Migration 017 backfills unambiguous existing investment links without changing financial values, balances, or financial-source revision. Ambiguous or inconsistent legacy links remain unchanged and require repair before pair mutation. The app records purchases and sales through the shared `/investment-trades` writer, with exact cash, included fees and validated execution facts. Unsupported standalone investment posting is read-only; legacy unverified pairs need reconciliation before mutation.

### Investments and currencies

Matching recorded purchases/sales now supply unit history directly; zero-start holdings need no separate opening date. A dated starting position is for imported units missing from the trade ledger. Verified old linked cash payments can support reporting cost, while standalone purchase currency/fees remain explicitly unavailable. See [recorded-history rules](docs/recorded-investment-history.md).

Market-priced holdings distinguish share/coin quantity from cash value. Their buy/sell history lives in `investment_transactions`; current portfolio valuation uses quantities, market quotes, and currency conversion. Manual assets use a separately managed value. Individual account trend charts keep the account's native currency, while aggregate views use the selected reporting currency.

Market quotes and exchange rates come from external services and may be unavailable or delayed. Investment and conversion behavior differs between some API, client, and MCP paths; contributors should follow the [financial rules in AGENTS.md](AGENTS.md#financial-rules-to-preserve) when changing calculations.
Market quotes and exchange rates come from external services and may be unavailable or delayed. The epic’s [market input contract](docs/investment-market-data.md) implements shared API/MCP quote normalization, bounded caching, actual source/session timestamps and dated ECB FX without a paid provider. Investment metrics use a shared calculator with documented complete/partial aggregate policies; contributors should follow the [financial rules in AGENTS.md](AGENTS.md#financial-rules-to-preserve) when changing calculations.

The investment rework's [versioned accounting contract](docs/investment-accounting.md) retains the dated pre-implementation audit and specifies opening quantities, exact cash/fees, remaining cost basis, manual valuations, historical FX, and estimated return calculations. Its [synthetic worked examples](tests/fixtures/investment-accounting-v1.json) are checked by the API suite. The current source implements those rules through shared calculators and guarded writers; see the [integrated validation and rollout plan](docs/investment-rework-validation.md) for evidence and remaining release steps.

The epic’s [holding model and account setup](docs/investment-holdings.md) implement explicit ETF/category/pricing selection, dated openings and safe current-position corrections. Migration 018 preserves existing balances and records while marking incomplete history. Cash-linked trades and dated manual values are implemented in their own epic steps; shared metrics and historical series consume that explicit opening coverage.

Ordinary cash writes, pending confirmation, linked cash-transfer edits/deletions and account adjustments use guarded atomic batches with decimal balance arithmetic and audit. Account snapshot guards and bounded retries preserve concurrent trades; stale account correction drafts retain their original saved balance/version. [Decimal balance recovery](docs/decimal-balances.md) handles narrowly verified old arithmetic residue during a successful mutation without rounding genuine fractional cash or crypto units.

### Recurring schedules

The API Worker runs recurring processing using the configured daily cron (`0 0 * * *`, midnight UTC). It creates posted transactions and updates balances for eligible schedules, respecting locks, archived accounts, end dates, remaining occurrences, and last-processed dates. Monthly days beyond the end of a month clamp to its final day.
The API Worker runs recurring processing using the configured daily cron (`0 0 * * *`, midnight UTC). It creates posted transactions and updates balances for eligible schedules, respecting locks, archived accounts, end dates, remaining occurrences, and last-processed dates. Its ledger writes, balances and occurrence consumption commit together, including under repeated execution. Monthly days beyond the end of a month clamp to its final day.

Yearly schedules keep their selected month across saves, API execution, client calendars, and MCP projections. Legacy schedules without a saved month fall back to their creation month until edited.

Expand All @@ -82,6 +92,8 @@ For forecasts, an AI client obtains context with `get_financial_outlook_context`

See the [MCP guide](mcp/README.md) for tools, authentication, setup, and the complete workflow. This project does not require an OpenAI API key or run a model itself.

Investment values, remaining cost, net invested and realized/unrealized gains use [shared accounting rules](docs/investment-metrics.md). Currency modes recompute acquisition-date FX; unknown legacy history stays unavailable. Market Today is a covered daily-close estimate. The combined [portfolio history chart](docs/investment-history.md) reconstructs dated units, prices, FX and appraisals, including sold and archived holdings, and reports covered range gain and daily-linked Estimated return. Missing history stays visible as gaps. The [responsive overview](docs/investment-overview.md) combines this chart with portfolio summaries, allocation and sortable holdings; category filters affect only the table, and unavailable values retain explicit coverage. The [holding panel and trade editor](docs/investment-detail.md) add native execution markers, current remaining-cost references, exact cash previews and version-protected linked edits/deletion.

## Architecture

```mermaid
Expand Down Expand Up @@ -129,7 +141,7 @@ Cloudflare Pages serves direct page paths using its SPA fallback (do not add a t
| API | Cloudflare Workers, Hono, Zod, TypeScript |
| MCP | Separate Cloudflare Worker with JSON-RPC tool handling and Access JWT verification |
| Storage | Cloudflare D1 (SQLite), ordered SQL migrations, audit records |
| Market data | Yahoo Finance via `yahoo-finance2`; exchange rates from `open.er-api.com` |
| Market data | Yahoo Finance HTTP adapter; current FX from `open.er-api.com`; dated ECB FX via Frankfurter |
| Verification | Vitest, Testing Library/jsdom, TypeScript, client ESLint, GitHub Actions |

```text
Expand Down Expand Up @@ -426,4 +438,10 @@ curl --fail-with-body \
- **Stale UI or data after deployment:** inspect service-worker caching; Settings includes cache management and force-reload controls. Cached assets do not provide offline transaction writes.
- **Empty or stale AI forecast:** a connected MCP client must publish a report first. Later ledger changes can make it stale; the UI does not automatically request a replacement.

For the derived metrics and nonmutating preview endpoints, see [Investment metrics](docs/investment-metrics.md).

For investment execution and migration 019, see [Direct cash investment trades](docs/investment-trades.md).

For implementation conventions, financial invariants, and change-specific checks, see [AGENTS.md](AGENTS.md). For historical changes and upgrade context, see [CHANGELOG.md](CHANGELOG.md).

For dated manual appraisals, whole-asset ownership/cash events and migration 020, see [Manual investment values](docs/manual-investment-valuations.md).
40 changes: 40 additions & 0 deletions api/migrations/017-investment-transfer-links.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- Cash rows already point to investment trades. Add the reverse link without
-- rebuilding either ledger or changing any recorded amounts/quantities.
ALTER TABLE investment_transactions ADD COLUMN linked_transaction_id TEXT REFERENCES transactions(id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_investment_transactions_cash_link
ON investment_transactions(linked_transaction_id) WHERE linked_transaction_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_transactions_linked_transaction_id ON transactions(linked_transaction_id);

-- Link metadata is not a financial-source change. Keep the existing revision
-- behavior for all pre-existing investment fields, including notes.
DROP TRIGGER IF EXISTS financial_revision_investment_transactions_update;
CREATE TRIGGER financial_revision_investment_transactions_update
AFTER UPDATE ON investment_transactions
WHEN OLD.id IS NOT NEW.id OR OLD.account_id IS NOT NEW.account_id OR OLD.type IS NOT NEW.type
OR OLD.quantity IS NOT NEW.quantity OR OLD.price IS NOT NEW.price OR OLD.total_amount IS NOT NEW.total_amount
OR OLD.date IS NOT NEW.date OR OLD.notes IS NOT NEW.notes OR OLD.created_at IS NOT NEW.created_at
BEGIN
UPDATE financial_data_revision SET revision = revision + 1,
updated_at = CAST(strftime('%s', 'now') AS INTEGER) * 1000 WHERE id = 1;
END;

-- Archived history needs the same metadata repair. Restore its read-only guard
-- immediately after the backfill; no account or financial fields are changed.
DROP TRIGGER IF EXISTS archived_investment_transactions_update;
UPDATE investment_transactions AS i SET linked_transaction_id = (
SELECT t.id FROM transactions t JOIN accounts a ON a.id = t.account_id
WHERE t.linked_transaction_id = i.id AND a.type != 'investment'
AND t.status = 'posted' AND t.date = i.date
AND ((i.type = 'buy' AND t.amount < 0) OR (i.type = 'sell' AND t.amount > 0))
)
WHERE (SELECT COUNT(*) FROM transactions t WHERE t.linked_transaction_id = i.id) = 1
AND EXISTS (SELECT 1 FROM accounts a WHERE a.id = i.account_id AND a.type = 'investment')
AND NOT EXISTS (SELECT 1 FROM transactions t WHERE t.id = i.id)
AND NOT EXISTS (SELECT 1 FROM investment_transactions other JOIN transactions t ON t.id = other.id
WHERE t.linked_transaction_id = i.id);

CREATE TRIGGER archived_investment_transactions_update
BEFORE UPDATE ON investment_transactions
WHEN EXISTS (SELECT 1 FROM accounts WHERE id = NEW.account_id AND archived_at IS NOT NULL)
OR EXISTS (SELECT 1 FROM accounts WHERE id = OLD.account_id AND archived_at IS NOT NULL)
BEGIN SELECT RAISE(ABORT, 'ACCOUNT_ARCHIVED'); END;
Loading
Loading