* Add missing IndexaCapitalItem::SyncCompleteEvent
Syncable#sync_broadcaster instantiates self.class::SyncCompleteEvent,
which is implemented for every other provider (Plaid, Lunchflow,
Mercury, etc.) but was missing for IndexaCapitalItem. The error was
swallowed by Sync#perform_post_sync's rescue, so syncs appeared to
succeed but post-sync UI broadcasts never fired:
Error performing post-sync for IndexaCapitalItem (...):
uninitialized constant IndexaCapitalItem::SyncCompleteEvent
This adds the class, modeled on LunchflowItem::SyncCompleteEvent,
restoring per-account and per-item Turbo broadcasts after Indexa
Capital syncs.
* Fix IndexaCapital account setup never creating accounts
complete_account_setup read params[:accounts], but the form in
setup_accounts.html.erb submits account_ids[] (array) and
sync_start_dates[<id>] (hash). The hash was always empty, so every
submit hit the empty-config branch and bounced back with
"No accounts to set up." — accounts were never created.
The controller also branched on config[:account_type] / config[:subtype]
even though the form has no account-type picker (Indexa Capital is an
investment-only broker). Rewrote complete_account_setup to consume the
form's actual params and infer the accountable type as Investment from
indexa_capital_account.account_type.
* Fix IndexaCapital balance double-count and account type
Two more issues in the IndexaCapital flow that surfaced once accounts
could actually be created (see prior commit):
1. Accountable type was inferred from indexa_capital_account.account_type
("mutual" / "pension"), but infer_accountable_type doesn't recognize
those values and falls through to "Depository". The result: every
imported Indexa account showed up as a Cash depository account
instead of an Investment account, hiding holdings/trades surfaces.
Indexa Capital is investment-only, so hard-code the accountable
type to Investment.
2. Account::Processor#calculate_total_balance summed every row in
raw_holdings_payload. Indexa returns a time series — one row per
security per date — so the naive sum double-counts (observed:
reported €91,633 became stored balance €180,039). Trust the API's
current_balance when present, and if we have to fall back to a
computed total, dedupe by instrument and take the latest-dated
amount per security.
* Fix IndexaCapital holdings reflecting oldest snapshot per security
HoldingsProcessor#process iterated every row in raw_holdings_payload.
Indexa returns a time series (many rows per security across dates),
and each iteration upserts the same (account, security, today) holding
row, so the LAST row processed wins. The payload is ordered with
newer dates first, so the last row processed is the OLDEST snapshot —
the holdings shown in the UI reflected tiny early positions instead
of the current ones (e.g. 3.8 shares of US 500 stored vs 62.34 actual).
Reduce the payload to one row per security (latest date) before
processing. The cost-basis update is now also driven by the latest
snapshot for the same reason.
* Fix IndexaCapital holdings using per-lot detail instead of totals
Importer#normalize_holdings_response read data[:fiscal_results], which
the Indexa API returns as per-tax-lot detail — many rows per security
covering each subscription_date, plus virtual sell/buy rows generated
by rebalances. Iterating it produced wildly wrong stored holdings:
e.g. 9.61 shares stored for Vanguard US 500 vs 62.34 actual; total
weights summed to ~10% instead of 100%.
The same response also includes data[:total_fiscal_results] — one
aggregated row per security with current titles/amount/cost matching
the Indexa UI and the user-downloadable positions CSV. Prefer it,
falling back to the per-lot field only when the totals are absent.
* Address CodeRabbit review on IndexaCapital fixes
Four review items, all fixed:
* Share instrument-key extraction
HoldingsProcessor#extract_ticker and Processor#calculate_holdings_value
used different fallback orders (one looked at :isin, the other at
:isin_code), so they could disagree on which rows referred to the same
security. Moved a single extract_instrument_key helper into
IndexaCapitalAccount::DataHelpers and routed both callers through it.
* Simplify Processor#calculate_holdings_value
The date-based dedupe was a workaround for the bug already fixed in
the importer (which now stores total_fiscal_results — one row per
security). Replaced the date comparison with a per-security map
populated via the shared key extractor. Same end result, fewer
moving parts, no fragile string-date comparison.
* Drop dead config key passed to create_account_from_indexa_capital
create_account_from_indexa_capital only reads :subtype and :balance
from its config arg. Passing :sync_start_date there was inert.
* Don't mark created accounts as skipped on post-create errors
In complete_account_setup, ensure_account_provider! and
update!(sync_start_date:) ran inside the same begin/rescue as the
Account.create!. If either raised after the Account row was already
persisted, control jumped to the rescue with created_count not yet
incremented and the account was wrongly counted as skipped. Now:
parse the form-supplied sync_start_date up front (a malformed value
is silently dropped instead of bubbling out of the loop), bump
created_count immediately after persisted?, and isolate the post-
create steps in their own rescue so failures there are logged but
don't desync the success counter.
* Fall back to /portfolio so pension plans get holdings imported
Indexa's /accounts/{id}/fiscal-results endpoint returns
{fiscal_results: [], total_fiscal_results: []} for pension plan
accounts (e.g. type "pension"). The same positions are exposed via
/accounts/{id}/portfolio in instrument_accounts[].positions[] for
both mutual funds and pensions, so use it as a fallback when
fiscal-results is empty.
The portfolio response uses the same field names HoldingsProcessor
already understands (instrument, titles, price, amount, cost_amount)
plus a derived cost_price (cost_amount / titles) added during
adaptation. No HoldingsProcessor changes needed.
Verified against the user-downloadable "Posiciones" CSV for an
SH71ZPMY pension account: two positions (N5138 Acciones, N5137
Bonos) and balance €8,273.56 match exactly.
* Fix CI: update tests for new IndexaCapital flow + rubocop blank line
* Lint: drop trailing blank line before `end` in
IndexaCapitalAccount::Processor (Layout/EmptyLinesAroundClassBody).
* Controller test: complete_account_setup#creates was posting
params: { accounts: { id => { account_type:, subtype: } } } against
the old controller schema. The new endpoint reads
params[:account_ids] and infers Investment for Indexa Capital, so
switch the test to that shape (and update the matching skip-already-
linked / no-selected-accounts cases).
* Processor test: "updates account balance from holdings value" set
current_balance: 38905.21 alongside holdings summing to 27093.01
and asserted the latter wins. After the fix
(calculate_total_balance prefers the API-reported current_balance
when present), the API value is the right answer. Renamed to
"trusts API current_balance over holdings sum when present" and
added a sibling test that nils current_balance to exercise the
holdings-sum fallback path explicitly (still asserts 27093.01).
* Wrap account creation+linking in a transaction to avoid orphans
complete_account_setup created the Account row first, incremented
created_count, and only then called ensure_account_provider! / the
sync_start_date update inside an inner rescue. If the link or the
sync_start_date update raised after the Account was already persisted,
control fell into the inner rescue: the orphaned Account row stayed
in the database, the failure was silently logged, and the success
counter was inflated.
Wrap creation, ensure_account_provider!, and the optional
sync_start_date update in a single ActiveRecord::Base.transaction.
Increment created_count only after the transaction commits; on any
exception the outer rescue rolls the whole step into skipped_count
with a clear log line tagged with the indexa_capital_account id.
.cursor/rules/*.mdc into single .junie/guidelines.md file (#343)
Deutsch | Español | Français | 日本語 | 한국어 | Português | Русский | 中文
Sure: The personal finance app for everyone
Get involved: Discord • Website • Issues
Important
This repository is a community fork of the now-abandoned Maybe Finance project.
Learn more in their final release doc.
Backstory
The Maybe Finance team spent most of 2021–2022 building a full-featured personal finance and wealth management app. It even included an “Ask an Advisor” feature that connected users with a real CFP/CFA — all included with your subscription.
The business end of things didn't work out, and so they stopped developing the app in mid-2023.
After spending nearly $1 million on development (employees, contractors, data providers, infra, etc.), the team open-sourced the app. Their goal was to let users self-host it for free — and eventually launch a hosted version for a small fee.
They actually did launch that hosted version … briefly.
That also didn’t work out — at least not as a sustainable B2C business — so now here we are: hosting a community-maintained fork to keep the codebase alive and see where this can go next.
Join us!
Hosting Sure
Sure is a fully working personal finance app that can be self hosted with Docker.
Forking and Attribution
This repo is a community fork of the archived Maybe Finance repo. You’re free to fork it under the AGPLv3 license — but we’d love it if you stuck around and contributed here instead.
To stay compliant and avoid trademark issues:
- Be sure to include the original AGPLv3 license and clearly state in your README that your fork is based on Maybe Finance but is not affiliated with or endorsed by Maybe Finance Inc.
- "Maybe" is a trademark of Maybe Finance Inc. and therefore, use of it is NOT allowed in forked repositories (or the logo)
Performance Issues
With data-heavy apps, inevitably, there are performance issues. We've set up a public dashboard showing the problematic requests seen on the demo site, along with the stacktraces to help debug them.
https://www.skylight.io/app/applications/s6PEZSKwcklL/recent/6h/endpoints
Any contributions that help improve performance are very much welcome.
Local Development Setup
If you are trying to self-host the app, read this guide to get started.
The instructions below are for developers to get started with contributing to the app.
Requirements
- See
.ruby-versionfile for required Ruby version - PostgreSQL >9.3 (latest stable version recommended)
- Redis > 5.4 (latest stable version recommended)
Getting Started
cd sure
cp .env.local.example .env.local
bin/setup
bin/dev
# Optionally, load demo data
rake demo_data:default
Visit http://localhost:3000 to view the app.
If you loaded the optional demo data, log in with these credentials:
- Email:
user@example.com - Password:
Password1!
For further instructions, see guides below.
Setup Guides
- Mac dev setup
- Linux dev setup
- Windows dev setup
- Dev containers - visit this guide
One-click
License and Trademarks
Maybe and Sure are both distributed under an AGPLv3 license.
- "Maybe" is a trademark of Maybe Finance, Inc.
- "Sure" is not, and refers to this community fork.