What investors see, after the portfolio rework
The four customer-facing investment routes, the two old paths that are now redirect stubs, which plan fields render on which screen, and where the admin shortcut points.
When a customer asks "where do I see my investment?", the answer changed. The old dashboard and the old history table were folded into one portfolio screen, and a per-investment page was added that never existed before. This page is the map: which route shows what, which old paths still work and why, and what each screen reads off a plan you configured.
Everything here is the general investment product that ships with core —
/investment, backed by Finance → Investment Management in the admin. AI
Investments, Forex and Staking are separate products, with their own customer
pages, their own tables and their own admin screens; nothing below applies to
them.
The four routes
| Route | Sign-in | What is on it |
|---|---|---|
/investment |
No | The product overview: a hero with up to three platform figures, a featured-plans strip, how a term settles, and what to expect |
/investment/plan |
No | Every plan with status true, as cards, with search and filter chips |
/investment/plan/<plan id> |
No to read, yes to buy | One plan in full, with the invest composer beside it |
/investment/portfolio |
Yes | Running positions first, settled positions below behind a disclosure |
/investment/<investment id> |
Yes | One position: principal, status, the maturity clock, and the Cancel control |
The first three are readable by a signed-out visitor, because the two endpoints
behind them (GET /api/finance/investment/plan and
GET /api/finance/investment/stats) declare no auth. That is deliberate: the
landing page is the marketing surface for the product, and it used to render
completely blank for anyone not signed in.
The overview's figures are conditional
The hero draws at most three numbers, from GET /api/finance/investment/stats,
and each one is omitted when it is zero:
| Figure | Source |
|---|---|
| Plans available | Count of investment_plan rows with status true |
| People invested | Distinct users holding an ACTIVE investment |
| Highest rate per term | MAX(profitPercentage) across active plans |
So a fresh install shows a hero with no statistics rather than a row of zeroes. If an operator expects figures and sees none, the cause is an empty book, not a broken endpoint.
totalInvested is computed by that endpoint but is not rendered anywhere.
It is SUM(amount) across every investment regardless of its plan's currency,
so it adds naira to tether; there is no honest single figure to print.
"Highest rate" is profitPercentage, which applies to the whole term whatever
its length. A 5% plan on a 7-day duration pays 5% over those seven days. Nothing
on these pages annualises it, and you should not either when answering a
customer.
/investment/dashboard and /investment/history are redirect stubs
Both paths still resolve. Both now redirect to /investment/portfolio, using
the locale-aware redirect so the customer's language segment survives the hop.
They are kept because they have been linked from investment emails, from
notifications and from customers' own bookmarks for as long as the product has
existed. Live in-app navigation points at /investment/portfolio directly — a
stub exists for links nobody controls any more, not as a routing layer.
What that means on a support ticket: a customer quoting /investment/history
is not on a stale build. They followed an old link, landed on the portfolio,
and everything they were looking for is on it — the settled section is the old
history table, and the running section is the old dashboard.
What was lost, deliberately: the old history table declared amount and
profit as plain number columns and disabled its own view dialog, so on desktop
there was no way to find out what currency a figure was in. Every figure on the
portfolio carries its unit.
The settlement notification now links /investment/<id>
The settlement cron's completion notification — and the View Investment
button on it — linked to /investments/{id}, plural. There is no
/investments route in this product and there never was. Every customer who
tapped that button, in every completion notification the platform has ever sent,
landed on a not-found page.
It now links /investment/<investment id>, singular, which is the per-position
page described below. The singular form was equally dead until this release,
because no per-investment page existed at all.
Notifications already delivered are not rewritten. If a customer complains that
the link in an old notification is broken, that is why; send them to
/investment/portfolio and the position is there.
The same notification is typed system (stored as SYSTEM, normal priority),
so it sorts with ordinary platform messages rather than above them.
The navigation, and who sees which item
The investment chrome renders three items, from frontend/app/[locale]/investment/menu.ts:
| Item | Href | Notes |
|---|---|---|
| Overview | /investment |
exact: true — without it the item would light up on every /investment/* route |
| Plans | /investment/plan |
|
| My investments | /investment/portfolio |
auth: true |
auth: true means the layout filters the item out entirely for a signed-out
visitor, rather than showing a door onto a sign-in wall. So a customer who says
"I can't find My investments" is almost certainly signed out — the item is not
hidden by permission, plan ownership or the feature switch.
There is deliberately no "Invest" item. Investing is an action, and it lives on every plan card and at the head of the portfolio, not in the nav bar.
Separately, the main site menu carries Investments → Investment Plans
pointing at /investment, and that entry is gated on the investment setting —
turn the feature off and the entry disappears from the site menu while the
routes themselves keep resolving. See
General Investment: enable and configure.
Menu labels resolve through the investment translation namespace with the
nav prefix, and the item's key is the lookup path — so Overview reads
investment.nav.overview.title. All three title keys are present in all 90
message bundles, and all 90 hold the English word. The same words already exist
translated as common.overview (89 locales) and common.plans (85), but the
menu translator can only reach one namespace, the one derived from the route, so
it cannot use them.
Until those keys are filled in, each label falls back to the English title in
the menu file. Adding a locale value to investment.nav.overview.title,
investment.nav.plans.title or investment.nav.portfolio.title does render
— the lookups are live, and the same holds for the three descriptions.
The admin shortcut points at Investment Plans
The header on these pages carries an admin shortcut, and it resolves to
/admin/finance/investment/plan.
The shortcut previously opened /admin/ai/investment. That is a different
product: its own tables (ai_investment, ai_investment_plan,
ai_investment_duration), its own routes, its own settlement cron and its own
permission keys. An operator following the shortcut from these pages arrived at
a console whose plans do not appear here and whose edits do nothing to any
position on these screens.
There is no /admin/finance/investment index page — the product has three leaf
screens and no landing screen — so the shortcut goes to plan, the screen that
owns everything the customer pages render.
Which plan fields render where
Both plan endpoints select the same twelve-ish attributes, and the two screens draw different subsets of them. Nothing else on the plan reaches a customer.
On a plan card (/investment/plan, and the featured strip on /investment)
| Shown | Field |
|---|---|
| Thumbnail, 56px square | image — rendered here for the first time; a missing file keeps the tile so the grid stays aligned |
| Title | title |
| Trending badge | trending |
| Description, clamped to three lines | description |
| The headline rate | profitPercentage |
| An outcome chip beside the rate | defaultResult |
| Minimum / Maximum | minAmount / maxAmount, with the plan's currency |
| Terms | The attached durations, written out by ICU as "30 days" / "3 months" — pluralised and localised, never the raw MONTH enum |
Cards sort trending first, then by rate descending. Filter chips are All plans, Trending (only when at least one plan is trending) and one chip per currency (only when more than one currency is represented) — so the control never offers a dead option.
On the plan page (/investment/plan/<id>)
Everything above except the trending badge, plus:
| Shown | Field |
|---|---|
| Currency | currency |
| Funded from | walletType — FIAT, SPOT or ECO |
| Terms as badges | The attached durations |
The picture is bounded both ways and hidden entirely when the file is missing, because a single-plan page has no grid alignment to preserve.
profitPercentage is a magnitude. defaultResult is the direction, and it is
what the outcome chip on both screens states. A plan at 12% with
defaultResult: LOSS deducts 12% of the principal at maturity; at DRAW it
pays nothing and returns the stake.
That field is served to the customer precisely so the pages cannot present a deduction as a gain. Set it deliberately on every plan — see Investment plans and durations.
Never served to a customer: name (the internal unique identifier),
invested (an admin-typed number that nothing increments — do not publish it as
social proof), minProfit and maxProfit (vestigial; nothing in the purchase
or settlement path reads them), and defaultProfit (a legacy fallback).
The single-plan endpoint ignores status
GET /api/finance/investment/plan filters on status: true. GET /api/finance/investment/plan/{id} does not, and neither does the purchase
endpoint.
So a plan you deactivate disappears from /investment/plan but its own page
still opens, still shows the composer, and still sells. Anyone holding the id —
a bookmark, an old email, a script — can keep buying it. If a plan must
genuinely stop taking money, read the deletion warning in
Investment plans and durations first.
The portfolio
Signed-in only. It loads the customer's investments and the plan catalogue — the list endpoint trims the embedded plan to four fields and sends neither the rate nor the settlement rule, so the catalogue supplies both.
- A summary strip, bucketed by currency. An investment carries no currency of its own; the denomination belongs to its plan, so positions in different currencies are never added together.
- Running, sorted by how close each is to paying, with a live countdown. One page-wide clock drives every countdown, so all of them read the same instant.
- Settled, below, behind a Show N settled disclosure.
Two behaviours worth knowing on a ticket:
- A position that crosses maturity in the browser is not settled by the browser. The page re-reads once at the boundary so the row stops describing itself as running, but the cron performs the settlement, on its own hourly schedule. Up to an hour between "0 remaining" and "completed" is normal.
- The store clears when the signed-in user changes. Signing out and back in as somebody else used to show the previous account's rows on the first paint.
The position page
/investment/<investment id> reads GET /api/finance/investment/{id} with
type=general, scoped to the caller. It shows the principal, a status badge, a
link to the plan, and a maturity panel with Opened, Matures (or
Matured), Term and Rate.
The Cancel control renders only while the position is ACTIVE, because that
is the only state the server accepts. Cancelling returns the full principal and
pays no ROI; the row is then soft-deleted, so the page redirects to the
portfolio rather than re-reading itself into a not-found panel.
The backend answers a missing row and a row belonging to somebody else with
the same 404 Investment not found, because the lookup is scoped to the
caller's id. Both land on a final "this position is gone" panel. Any other
failure lands on a retry panel instead. If a customer reports "my investment
disappeared", check whether they are signed in as the account that opened it
before you go looking at the database.
Related
- General Investment: enable and configure — the feature switch, the KYC gate, the cron and the permission keys.
- Investment plans and durations — every field on a plan and which two decide the payout.
- The general investment lifecycle — what happens between the purchase and the payout.
- The investment history desk — the admin side of the same positions.
- General Investment troubleshooting — when a position does not settle.
- General Investment endpoints, tables and keys — the API reference behind these screens.