The investment history desk, and the three actions that lie

What each metric on the investment history screen means, and why the status, create and delete actions behind it write columns without moving a single unit of anybody's money.

6 min readUpdated 6 August 2026investment, payouts, settlement, audit, danger

/admin/finance/investment/history is the record of every general investment ever opened. It is the screen you land on when a customer says they have not been paid, and it is the most dangerous screen in the investment product — because three of the actions behind it look like settlement controls and are not.

Read the danger section before you touch a row. Everything else on this page is about reading the screen correctly.

Path /admin/finance/investment/history (menu: Finance → Investment Management → Investment Analytics)
Model investment, paranoid — deletes are soft and there is a Show deleted view
Buttons on the table Create off, Edit off, Delete on, View on
Permissions access.investment · view.investment · create.investment · edit.investment · delete.investment

Rows carry the customer, the plan, the duration, the amount, the profit, the result, the status, the maturity date and the creation date. Opening a row shows the money as a stat strip — invested, profit, ROI, total return — with the plan term and the record's timeline below it.

The three actions that lie

Sets one investment's status
Sets many investments' status

Both of these call one shared helper that writes a single column and returns "updated successfully". There is no wallet credit, no transaction row, no result, no ROI, no email and no notification. The customer's balance does not move. The audit trail records that you changed a status, which is exactly and only what happened.

The damage is that the row now reads COMPLETED to everyone who looks at it afterwards — including you, next week, and including the settlement job, which only ever loads rows with status: ACTIVE. Marking an unpaid position COMPLETED permanently removes it from the queue of things that will ever be settled. The money is not lost, but nothing will pay it out on its own again, and nothing on any screen will tell you.

The bulk version does the same thing to every selected row at once.

Inserts an investment row

A bare insert of userId, planId, durationId, amount, profit, result, status and endDate. No wallet is debited, no funding transaction is written, and none of the purchase route's checks run — no feature switch, no KYC gate, no minimum or maximum, no balance check, no "one active per plan" rule.

You have created an obligation the platform will honour with real money. If you insert it ACTIVE with a past endDate, the next hourly settlement run credits the customer principal plus ROI out of the platform's own funds, for a principal that was never collected.

The table's Create button is deliberately off, so this is only reachable through the API — but the permission create.investment exists and is grantable, and some admins do reach it. Do not grant it.

Deletes an investment record

The row menu's Delete is the platform's generic soft delete. It stamps deletedAt and nothing else — no refund path, no reversal of the funding debit, no transaction row. The customer's principal stays where the purchase put it: with you.

Permanent Delete in the same menu sends force=true and destroys the row outright, which also destroys the only record of what was owed.

Deleting the row also removes the position from the analytics header, so the outstanding capital figure you rely on quietly shrinks by an amount you are still liable for.

There is also a row-edit endpoint (PUT .../history/{id}, edit.investment) which writes the same eight columns directly. The table's Edit button is off, and the same warning applies: it changes what the record says, not what happened.

What to do instead

  1. Let the cron settle it. processGeneralInvestments runs hourly and pays every ACTIVE position whose stored endDate has passed. If a position is overdue, the question is why the job is not settling it, not how to close the row by hand.

  2. Check the scheduler first. System → System Monitoring → Scheduled Tasks, the Process General Investments job. A failed run means at least one customer has not been paid, and the cron log names the investment id. The usual cause is a missing wallet in the plan's currency and wallet type.

  3. Check the plan still exists. A soft-deleted plan makes the job skip every position on it, silently, on every run. Restore the plan (Show deleted on the Investment Plans screen) and the next run settles them.

  4. If a term needs to end early, move the date. endDate is the only field on this record it is safe to adjust, because settlement reads the stored value rather than recomputing it. Set it into the past and the next hourly run settles the position properly — real credit, real transaction, real email.

  5. If a customer wants out with no ROI, have them cancel. The customer-side cancel on /investment/<id> refunds the full principal as a REFUND transaction, annotates the original debit, and soft-deletes the position. There is no admin equivalent, and nothing on this desk reproduces it.

  6. If you must move money by hand, do it on the wallet. Finance → Transaction Management → Wallets has a balance adjustment that writes a real ledger entry, carries an idempotency token and can notify the customer. That is the only supported way to credit or debit someone from the admin.

Reading the analytics header

Every currency figure is converted to USD through the plan's currency, one row at a time, because an investment carries no currency of its own — the denomination belongs to its plan. Amounts in a plan currency with no available rate are named on the card rather than folded in as zero.

Liability, and the alarm

Card What it is
Capital Deployed (activeCapital) Outstanding principal across every ACTIVE position, ignoring the date filter. This is what you owe.
Overdue Capital (overdueCapital) The slice of that which is past its endDate by more than an hour.
Overdue Settlements (overdueCount) How many positions that is.

Those last two are the settlement-stall alarm, and they are the only figures on this screen that need action the same day. A row still ACTIVE after its maturity date is a settlement the cron did not perform — customer money held past its term. The one-hour grace keeps positions maturing right now out of the count.

If overdue capital is non-zero and rising, go to Scheduled Tasks. Do not close the rows.

The book's result

Card What it is
ROI Paid Out (roiPaid) Sum of profit on COMPLETED positions with result WIN — money the platform paid out.
Principal Retained (roiKept) Sum of profit on COMPLETED positions with result LOSS — principal the platform kept.
House Result (houseResult) Retained minus paid. Positive means the book made money.

Flow and quality

Card What it is
Win Rate WIN as a share of COMPLETED.
Winning / Completed The two counts it is built from.
Average Ticket Mean amount across ACTIVE and COMPLETED positions only — cancelled and rejected rows never became a position. Deliberately unitless: the mean of a naira ticket and a tether ticket is not a figure in either currency, so it carries no currency symbol.
Capital Invested (period) Principal placed inside the selected timeframe, in USD.

Three charts sit alongside: principal invested per bucket, win rate over time, and the top five plans by outstanding principal with everything else rolled into Other.

The currency conversion joins each position to its plan, and that join is required while investment_plan is paranoid. Positions whose plan has been soft-deleted therefore drop out of every figure above, including Capital Deployed. Retiring a plan makes your outstanding liability look smaller than it is. Deactivate plans rather than deleting them, and see Investment plans and durations.

Statuses you will see

Status Meaning here
ACTIVE Running, or matured and waiting for the next hourly run
COMPLETED Settled by the cron — or set by hand, which is not the same thing
CANCELLED Defined on the model; the customer cancel path soft-deletes instead, so this is only ever written by an admin
REJECTED Defined on the model; nothing in the product writes it
Result Meaning
WIN Principal plus ROI was paid
LOSS Principal minus ROI was paid, floored at zero
DRAW Principal only was returned

result is null on every row until settlement writes it. A row showing COMPLETED with no result was closed by hand, and is the signature of the mistake this page exists to prevent.