MMashDiv

Investments

The full lifecycle of a forex investment — what the create route checks, what the hourly cron pays out and how it computes it, the four statuses, refunds on cancellation, and recovering one that failed.

12 min readUpdated 28 September 2026investments, settlement, cron, refunds, lifecycle

An investment is a principal, a plan, a duration and an end date. It has four statuses and exactly two ways out: it settles, or it is cancelled and refunded. Nothing else may take money off it.

Opening one

  1. Open a plan card — the invest form opens from here

The customer opens /forex/plan, picks a plan, picks one of the durations that plan offers, enters an amount and accepts the terms. Before a row is written, the request passes eight checks in order:

  1. KYC — the customer's verification level must carry invest_forex.

  2. Terms accepted — the request is refused without it, and the acceptance timestamp and version are stored on the investment.

  3. The plan is enabled — a plan an admin has switched off is refused with This plan is not currently accepting investments. Its existing investments continue to settle.

  4. The amount is a positive, finite number — checked before the limits, because the limit comparison is skipped on a plan whose minimum is zero. A negative amount used to pass every check and credit the forex account, which let anyone with a live account mint withdrawable money.

  5. The amount is within the plan's limits — minAmount and maxAmount.

  6. The duration is offered by this plan — the forex_plan_duration join is consulted. A duration that exists but is not attached to this plan is refused.

  7. Fraud checks — see below.

  8. The forex account can pay — inside a transaction, with the account row locked: it must be a LIVE account, its currency and wallet type must match the plan's, and its balance must cover the principal.

Only then is the row created, the principal debited, and an audit transaction of type FOREX_INVESTMENT written against the customer's matching wallet. The investment starts ACTIVE with its end date already computed.

The creation endpoint is rate-limited to 10 investments per hour per caller.

The fraud checks

Two hard limits, neither of them configurable:

Check Refuses
More than 10 ACTIVE investments already held Too many active investments
Amount above the plan's own maxAmount Investment amount exceeds this plan's maximum

The second is worth noting because it used to be a hidden literal of 50,000 that overruled the plan: a plan an admin had deliberately configured with a maximum of 100,000 rejected a legitimate 60,000 investment, with a security-sounding message that gave no hint the admin's own setting had been overridden.

If a fraud check itself errors — a database problem, a missing price — the request is allowed. The checks are a filter, not a gate.

The four statuses

Status Means Holds the principal?
ACTIVE Running. The principal has left the customer's forex account Yes
COMPLETED Settled. The result was paid into the forex account No
CANCELLED Abandoned. The principal was refunded No
REJECTED Same as cancelled, with a different label No

Only ACTIVE is a state where money is at stake. Everything that moves an investment out of ACTIVE without settling it has to hand the principal back, and every admin path that does so goes through one shared transition that performs the refund in the same transaction as the status change.

Settlement

The hourly processForexInvestments cron does all of it. Each run loads every ACTIVE investment with its plan and duration, and for each one whose stored end date has passed:

  1. Lock the row and re-check. An investment that is no longer ACTIVE is skipped. This is what makes a manual cron trigger racing the scheduled tick safe.

  2. Find the customer's LIVE forex account and lock it. If there is none, nothing is paid and the investment is left ACTIVE for manual review.

  3. Compute the movement. principal × profitPercentage ÷ 100, taking the percentage from the plan the customer was quoted.

  4. Decide the result. The investment's own result column if an admin set one, otherwise the plan's default result.

  5. Flip the status to COMPLETED, conditionally on it still being ACTIVE. That single conditional update is what makes the payout idempotent — if another run already settled it, zero rows match and nothing is paid.

  6. Credit the forex account.

    • WIN — principal plus the profit
    • LOSS — principal minus the loss, floored at zero
    • DRAW — the principal
  7. Record the platform's side — the payout on a win, the retained principal on a loss — so the addon's contribution to profit and loss is visible per currency. This step is best-effort by design: a problem writing it can never strand a settled investment or hold up the customer's payout.

  8. Pay any affiliate reward, then email and notify. The affiliate credit runs first and in its own error handler, because it is a financial credit to a third party and must not be skipped because an email failed.

A loss is written as a negative number. Rows settled before that was true hold the loss as a positive number beside a LOSS result, which is why every reporting surface in the addon applies a sign correction when it sums the column. If you query forex_investment.profit directly, apply the same rule: a LOSS row's profit is the absolute size of the loss, negated.

Payouts go to the forex account

Not to the spot wallet. The principal left the forex account, so the return belongs there, and the customer takes it out through the normal withdrawal queue like any other forex balance.

This matters operationally: a matured investment is not money the customer has received. It is money sitting in a sub-account waiting on your approval. A busy settlement day produces a busy withdrawal queue the following morning.

When settlement fails

Each investment gets up to three attempts with exponential backoff. If all three fail, the cron cancels the investment and refunds the principal in the same transaction, records how much it refunded in the investment's metadata, and notifies the customer. If a safe refund is impossible — there is no LIVE account to return the money to — the investment is left ACTIVE and an alert is logged rather than being cancelled with the money stranded.

Two deterministic failures are handled separately and never enter the retry path: an investment whose plan or duration cannot be resolved (usually a soft-deleted plan) is logged and left ACTIVE for manual resolution, because retrying it three times and then cancelling would be worse than leaving it alone.

Admin actions

  1. Status — only ACTIVE is a state where money is at stake
  2. The bulk selector goes through the same refunding transition as the row action

Admin → Forex → Investments (/admin/forex/investment).

Editing

Every field on the edit dialog saves as before except the amount, which is fixed once the investment exists.

The principal has already been taken from the customer's forex account. Changing the amount would change what the investment pays at maturity without changing what was collected — raising a 1,000 investment to 10,000 returned 10,000 against the 1,000 that had actually been paid in, and lowering it did the reverse and kept the difference.

The route refuses the change and tells you what to do instead: cancel the investment, which refunds the principal, and create a new one.

Setting result on an individual investment before it settles is how you make one investment differ from the plan's default outcome.

Cancelling

Three doors set the status — the single status endpoint, the edit dialog and the bulk selector — and all three now go through the same refunding transition. Only one of them used to refund; the other two stranded the customer's money.

The transition is safe to repeat: the status flip is conditional on the status you read, so a second click is a no-op rather than a second refund. It refunds only when the investment was ACTIVE, because one that is already cancelled was refunded when it got there and a completed one has been paid out.

If the customer has no LIVE forex account to refund to, the cancellation is refused rather than silently discarding the principal.

What the status actions refuse

The status actions make exactly two moves: an ACTIVE investment to CANCELLED or to REJECTED, both of which refund the principal. Setting the status an investment already has changes nothing. Every other move is refused with a 400 and the reason, and nothing is written. That applies to the row action and the edit dialog. The bulk selector moves the rows it can and names each refused row beside its reason.

Refused move Why
CANCELLED to anything The principal has already been refunded. Setting it back to ACTIVE would leave that refund with the customer and pay the principal again at maturity. Recover is the only way back to ACTIVE, because it takes the refund back first
COMPLETED to anything It has already been paid out. Setting it back to ACTIVE, or to CANCELLED and then recovering it, would settle it a second time, principal and profit
REJECTED to anything Final. Its principal was refunded when it was rejected
ACTIVE to COMPLETED Settlement is the only thing that completes an investment, because it pays it out. Marked by hand, the investment is recorded as paid while the customer receives nothing, and no screen can pay them afterwards. To end one early, cancel or reject it, which returns the principal

The edit dialog changes the status before it saves the other fields, and the check is made again on the locked investment. A refused status change therefore discards the other fields in that save, even when the investment changed while the dialog was open. If one save reassigns an investment to another customer and cancels it, the principal goes back to the customer who paid it.

Cancelling or rejecting an ACTIVE investment is also refused when the customer's own cancellation already returned its principal to their wallet, because refunding it again would pay the principal twice. That is an investment set back to ACTIVE on an older version after the customer cancelled it, or one whose cancellation did not finish: the refund reached the wallet but the investment still reads ACTIVE. Such an investment still settles at maturity, so reconcile it with the customer before then.

Older versions accepted any move on all three status actions. An investment that was set back to ACTIVE after a refund or a payout, or moved from COMPLETED to CANCELLED, may already have been paid twice. Investments cancelled from the single-row status action on an older version also carry no principalRefunded record, so Recover cannot see their refund. Check both groups against the customer's forex account before you recover any of them.

Two more groups date from older versions. An investment the customer cancelled themselves, restored from the deleted records and then recovered was paid twice. An investment created from the admin endpoint as COMPLETED, CANCELLED or REJECTED had its principal taken, and no action pays it out or refunds it.

Deleting

Deleting an ACTIVE investment is refused, and the message tells you how much principal the selection is holding. Deletion would take the investment out of the settlement cron's reach and the money would never come back. Cancel first — that refunds — then delete if you want the row gone.

Recovering

CANCELLED investments can be re-armed with Recover, which needs edit.forex.investment.

A cancelled investment has almost always already been made whole — the cron's terminal-failure path refunds the principal and records the amount in the investment's metadata. Re-arming it and settling it again would pay the principal twice: once as the refund, once as the maturity payout.

Recovery therefore takes the refund back out of the customer's forex account before flipping the investment to ACTIVE. If the account can no longer cover it, recovery is refused with the two figures named, and you are told to have the customer restore the balance or leave the investment cancelled.

Two admins clicking Recover at the same time cannot both reclaim: the flip is conditional on the investment still being CANCELLED.

An investment the customer cancelled themselves cannot be recovered. Their cancellation returned the principal to their wallet, not to the forex account that Recover reclaims from. Restoring the investment from the deleted records does not change that: Recover refuses it, because settling it would pay the principal a second time.

Creating from the admin side

There is no Create button on the investments table, but the endpoint exists and behaves exactly like the customer path: it locates the customer's LIVE forex account, checks the denomination against the plan, and debits the principal before writing the row. The investment is always created ACTIVE. Any other status is refused, because only an ACTIVE investment is ever settled or refunded. To end one, create it and then cancel or reject it.

That last part is why the button is worth avoiding unless you know what you are doing. The route used to write the row and touch no balance at all, so an investment created with a past end date paid out both principal and profit on the next cron tick with nothing having been paid in — anyone holding create.forex.investment could mint spendable balance.

What the customer sees

  1. Status — ACTIVE, COMPLETED, CANCELLED or REJECTED
  2. An unsettled investment shows no profit or loss at all

/forex/investment lists their investments; /forex/investment/{id} shows one in full with its plan, duration and result. /forex/dashboard carries totals, a plan distribution and recent activity. All of it is scoped to the caller — there is no way to read another customer's investments.

An investment that has not settled shows no profit or loss, rather than +0.00 in green.