All use cases
Build this app

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.

Your network6 members

Commission statement

Reversed rows are struck through and excluded from the paid total.

August statementPaid
LevelRecipientTierAmountStatus
1Ken Mbeki240.5paid
1Priya Raman198paid
2Tom Alvarez64.25pending
2Jo Fenwick31reversed
Total (paid)438.5

Rank progress

Thresholds come from your compensation plan, not the component.

Rank progress
ManagerDirector
6,400 / 10,000 TV · 3,600 to Director

The code

Two pointers, because they answer two questionstsx
// `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"
A commission run you can re-explain laterphp
// 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',
    ]);
}
Reversal is a status, not a deletephp
// 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

  1. 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-mlm
  2. Decide 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.

  3. Compute rewards per order

    One computation per reward, carrying level, metric and recipient. That record is what the statement is rendered from later.

  4. Model reversal as a status

    A reversed row that stays visible is the difference between a statement someone trusts and one they dispute.

  5. Show rank against a real threshold

    Qualification thresholds come from your compensation plan, not the component — so changing the plan is a config change.