A referral or network-marketing platform
Downline trees, commission runs and rank qualification — the parts that are hard to get right and expensive to get wrong.
The problem
Network compensation is money arithmetic over a graph, and both halves are unforgiving. The graph has two different parent pointers — who enrolled you, and where you sit — and they are not the same edge. Drawing one when you meant the other is why so many downline views are quietly wrong.
The money is worse. Commissions accrue per level, get held, and get reversed when an order refunds. Someone will ask you to explain a specific number months later, and a statement that cannot show a reversal is a statement that disagrees with the bank.
What you are building
Live surfaces, not screenshots — every one composed from the same components you would install. A PHP engine with a matching Node port, and a React surface. Unilevel, binary and matrix trees all render from the same JSON.
Downline tree
fancy-mlm-ui renders unilevel, binary and matrix trees from the same JSON.
Commission statement
Reversed rows are struck through and excluded from the paid total.
| Level | Recipient | Tier | Amount | Status |
|---|---|---|---|---|
| 1 | Ken Mbeki | 240.5 | paid | |
| 1 | Priya Raman | 198 | paid | |
| 2 | Tom Alvarez | 64.25 | pending | |
| 2 | Jo Fenwick | 31 | reversed | |
| Total (paid) | 438.5 | |||
Rank progress
Thresholds come from your compensation plan, not the component.
The code
// `sponsorId` is who enrolled you; `placementId` is where you sit in the tree.
// They diverge constantly in binary and matrix plans, and the component takes
// either -- so which tree you draw stays a data decision, not a rewrite.
const members = [
{ id: "m1", label: "Rosa Delgado", tier: "Director" },
{ id: "m2", label: "Ken Mbeki", tier: "Manager", sponsorId: "m1" },
{ id: "m3", label: "Priya Raman", tier: "Manager", sponsorId: "m1" },
];
<DownlineTree value={members} rootId="m1" edge="sponsor" /> // or edge="placement"// The engine returns a computation PER REWARD rather than one total, so a
// statement can show why a number is what it is: level, metric, recipient.
$rewards = Mlm::computeRewards($order, [
'levels' => [1 => 0.10, 2 => 0.05, 3 => 0.02],
]);
foreach ($rewards as $reward) {
Commission::create([
'member_id' => $reward->recipientMemberId,
'level' => $reward->level,
'amount' => $reward->amount,
'status' => 'pending',
]);
}// Refunds land after payout runs. Marking rather than deleting is what keeps the
// statement explainable: the row is struck through and excluded from the paid
// total instead of vanishing and leaving an unexplained gap.
public function reverseFor(Order $order): void
{
Commission::where('order_id', $order->id)
->where('status', '!=', 'paid')
->update(['status' => 'reversed', 'reversed_at' => now()]);
}How to solve it
Install the engine and the surface
The engine ships as a matched PHP and Node pair, so the same compensation rules run whichever backend you have.
Run thisbash composer require particle-academy/fancy-mlmDecide which edge your plan draws
Unilevel usually means sponsor; binary and matrix mean placement. Get this wrong and every downline view is subtly incorrect while looking fine.
Compute rewards per order
One computation per reward, carrying level, metric and recipient. That record is what the statement is rendered from later.
Model reversal as a status
A reversed row that stays visible is the difference between a statement someone trusts and one they dispute.
Show rank against a real threshold
Qualification thresholds come from your compensation plan, not the component — so changing the plan is a config change.
