* fix(onchain-wallets): call transfers transfers, and drop the address swap
Three things reported from real use.
**A transfer was announced as a purchase.** A movement was imported with
`activity_label: "Buy"/"Sell"` and named "Buy 1.5 FAKE" — but coins arriving at
an address were not bought there, and nothing here knows whether they were ever
bought at all. The trade shape stays, because it is what carries quantity and
cost basis in this ledger, but the label is now "Transfer" and the name is the
one the movement already had while it was unpriced. The old wording also made
the same event rename itself the day a price turned up for it.
Worth pairing with the change to trades/_header.html.erb, which until now read
the amount's sign and would still say "Buy" whatever the label.
**Changing a tracked address is gone.** It repointed the rows at a new address
while keeping their accounts, holdings and history — so trades reconstructed
from address A stayed under an account presented as address B. Its own help
text said so out loud: "The accounts, holdings and history stay as they are."
Removing the address and adding the new one is not just simpler, it is the only
one of the two that is honest, because it takes the old history with the old
address.
**The buttons follow the app's conventions now.** Actions that repeat per row
belong in a menu here — accounts/_account.html.erb renders its own that way —
not in a row of labelled buttons, which is what a provider panel does when it
has a single connection to act on. So the per-address actions are a menu, the
per-asset disconnect is icon-only, and the accounts-page card gains the actions
menu every other provider card already had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(onchain-wallets): give a tracked asset its icon on the accounts page
Account#logo_url asks its provider adapter for one, and ours answered nothing:
there is no institution behind a self-custody wallet, and nothing attaches a
file, so every tracked asset showed a blank where every other account shows an
icon. Fixing Security#crypto_base_asset covered the holdings list, which reads
the security directly — this is the other path, and it went through the adapter.
Built from the symbol rather than looked up. The accounts page renders one of
these per account, so resolving a Security each would be a query per row, and
the symbol is all Brandfetch's crypto endpoint needs. It answers nil without a
client id, which is the same nothing the page shows today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(onchain-wallets): follow the moved banner and the menu in the browser
The system suite still expected the settings panel to carry the read-only
reassurance, and to find "Review tokens" as a visible button. Both moved in the
previous commit: the banner into the linking modal, where the question it
answers is actually asked, and the per-address actions into a menu, as repeated
row actions are rendered everywhere else in this app.
Caught by CI rather than by me — the per-branch checks I ran covered
`bin/rails test` and not `test:system`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(onchain-wallets): pin the reassurance to the frame it moved into
The assertion proved the banner was somewhere on the page, which is exactly
what the change does not claim: the point is where it lives. Now it asserts the
text is absent from the settings panel and present inside the modal, so the
test fails if the banner drifts back or never arrives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(onchain-wallets): rename movements imported before transfers had a name
Review on #3153.
Wallets synced before this change keep their `Buy`/`Sell` labels and their
"Buy 1.5 shares of CRYPTO:BTC" wording, and nothing was rewriting them:
`perform_sync` returns early when no address changed on chain, and the
repair pass only ever looked at display-only `Transaction` rows. A cold
address would have shown the old wording indefinitely — which for a wallet
nobody touches is most of them.
The repair now relabels this processor's own trades too, scoped to its
`external_id` prefix and to `source: SOURCE` so a trade the user entered by
hand is never renamed. It runs from `perform_post_sync`, which is the pass
that already runs for every linked asset rather than only the changed ones.
Idempotent, so a nightly sync does not rewrite the same rows forever.
Separately: `Security.brandfetch_crypto_url` interpolated the symbol
straight into a URL path, and `Onchain::AssetSymbol.canonical` only upcases
and trims. An on-chain token can be called whatever its deployer chose, so
a slash pointed the path elsewhere on the CDN and a hash pushed the client
id into a fragment Brandfetch never sees. Guarded in the helper rather than
at the call site — six callers reach it from four providers.
Also from review: the icon test saves and restores
`Setting.brand_fetch_client_id` instead of hard-coding nil in its `ensure`,
which was erasing whatever the suite had configured.
The "one query" claim in a repair test's name was never asserted, and this
change adds a second query. Renamed to what it actually checks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye
* test(onchain): drop the last change_address test with its feature
#3182 added a `change_address` test while this branch was removing the
feature it exercises. The rebase kept both, leaving a test calling a route
this branch deletes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Security#crypto_base_asset only stripped a fiat suffix — "BTCUSD" gave "BTC" —
so it answered nil for the "CRYPTO:BTC" form, which is the form every crypto
integration writes: the on-chain wallets, Kraken, CoinStats and Binance all
store the prefix. display_logo_url feeds that nil to the crypto branch, so none
of those securities carried a logo at all.
Provider::BinancePublic already parses every shape it accepts — the pair form,
the prefixed pair, the bare base asset and the USD stablecoins — so this
delegates instead of growing a second parser next to it. The parsing moves to a
class method for that; the private instance method stays as a delegator, so its
own callers and their tests are untouched.
Verified across the four documented forms: CRYPTO:BTC and BTCUSD both give BTC,
CRYPTO:ETH gives ETH, CRYPTO:USDT gives USDT.
A logo still needs BRAND_FETCH_CLIENT_ID configured — this fixes the parsing,
not the hosting requirement.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds Provider::TinkoffInvest, a token-based securities provider built on the
public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves
prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand
logos via the T-Invest CDN — the authoritative logo source for MOEX
instruments, which ISS (MoexPublic) does not provide.
- Registry: register `tinkoff_invest` under the :securities concept; token via
ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key.
- Logos independent of the price provider: Security#import_brand_logo consults
T-Invest for a logo whenever a token is configured (after the price-provider
metadata fetch, so it never short-circuits website_url backfill). Gated on
token presence, not the securities checklist.
- display_logo_url: with no website domain, a stored provider logo (T-Invest)
now beats the ticker-only Brandfetch lettermark; when a domain exists,
Brandfetch still wins (unchanged).
- MoexPublic no longer reports moex.com as the issuer website — it's the
exchange, not the issuer, and would make Brandfetch render the exchange logo
for every instrument and shadow the real brand logo.
- Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9;
bonds priced as percent-of-par x nominal (missing nominal raises, not 0).
- Settings: encrypted token field (always shown) + provider checkbox + en locale.
- Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic.
Co-authored-by: Claude <noreply@anthropic.com>
* Binance as securities provider
* Disable twelve data crypto results
* Add logo support and new currency pairs
* FIX importer fallback
* Add price clamping and optiimize retrieval
* Review
* Update adding-a-securities-provider.md
* day gap miss fix
* New fixes
* Brandfetch doesn't support crypto. add new CDN
* Update _investment_performance.html.erb
* Record dividends and interest as Trades in investment accounts
All investment income (dividends and interest) is now modeled as a
Trade with qty: 0 and price: 0, keeping security_id NOT NULL on trades
intact. Dividends require a security; interest falls back to a
per-account synthetic cash security (kind: "cash", offline: true) when
none is selected, matching how brokerages handle uninvested cash
internally.
- Add `kind` column to securities ("standard" | "cash") with DB check
constraint; `Security.cash_for(account)` lazily finds or creates the
synthetic cash security; `scope :standard` excludes synthetic
securities from user-facing pickers
- Trade::CreateForm: new `dividend` type (security required); `interest`
now creates a Trade instead of a Transaction
- Trade form: Dividend and Interest in the type dropdown with a security
combobox (required for dividend, optional for interest)
- transactions table: untouched
* UI fixes
* HealthChecker — both scopes now chain .standard to exclude cash securities from provider health checks.
DB query moved to model — Account#traded_standard_securities in app/models/account.rb, view uses account.traded_standard_securities.
DRY income creation — create_income_trade(sec:, label:, name:) extracted as shared private method; create_dividend_income and create_interest_income delegate to it.
show.html.erb blocks merged — single unless trade.qty.zero? block covers qty/price/fee fields.
Test extended — assert_response :unprocessable_entity added after the assert_no_difference block.
* Hide cash account ticker from no-security trade detail
* Fix CodeRabbit review issues from PR #1311
- Remove duplicate YAML keys in translation files (de, es, fr)
- Add error handling for security resolution in create_dividend_income
- Extract income trade check to reduce duplication in header template
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* Include holdings in dividend/interest security picker
The security picker for dividend/interest trades should include all securities
in holdings, not just those with trade history. This fixes the issue where
accounts with imported holdings (e.g., SimpleFIN) but no trades would have an
empty picker and be unable to record dividends.
Uses UNION to combine securities from both trades and holdings.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* scope picker to holdings only (a trade creates a holding anyway)
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Since the very first 0.1.0-alpha.1 release, we've been moving quickly to add new features to the Maybe app. In doing so, some parts of the codebase have become outdated, unnecessary, or overly-complex as a natural result of this feature prioritization.
Now that "core" Maybe is complete, we're moving into a second phase of development where we'll be working hard to improve the accuracy of existing features and build additional features on top of "core". This PR is a quick overhaul of the existing codebase aimed to:
- Establish the brand new and simplified dashboard view (pictured above)
- Establish and move towards the conventions introduced in Cursor rules and project design overview #1788
- Consolidate layouts and improve the performance of layout queries
- Organize the core models of the Maybe domain (i.e. Account::Entry, Account::Transaction, etc.) and break out specific traits of each model into dedicated concerns for better readability
- Remove stale / dead code from codebase
- Remove overly complex code paths in favor of simpler ones