Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

NoLAstNamE's avatar

adding amount in parents recursion

Good day everyone, I recently posted a question here regarding adding the amount to the next parent in loop and it goes well with the help of @rodrigo.pedra, he really helped me a lot and the code below is the result of that, it's been a week now after I posted the origianal question below:

https://laracasts.com/discuss/channels/laravel/laravel-add-amount-to-the-next-parent-in-loop

The below code is working like this:

  1. Jade sent a link to a person and that person order without logging in (Guest)
  2. After making an order the earnings will insert to the parents with rank in chain of Basic -> Junior -> Premium -> Advanced -> Senior
  3. In that case earnings will insert like this:
Jade = 50
Joey = 100
Josh = 150
Jane = 200
John = 250

Each rank does have a corresponding value

Basic = 50
Junior = 100
Premium = 150
Advanced = 200
Senior = 250

users table

select `id`, `parent_id`, `rank`, `name` from `users`

+--+---------+--------+----+
|id|parent_id|rank    |name|
+--+---------+--------+----+
|1 |NULL     |Senior  |john|
|2 |1        |Advanced|jane|
|3 |2        |Premium |josh|
|4 |3        |Junior  |joey|
|5 |4        |Basic   |jade|
+--+---------+--------+----+

Earning Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Earning extends Model
{

}

User Model

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    protected const RANKS = [
        'Guest',
        'Basic',
        'Junior',
        'Premium',
        'Advanced',
        'Senior',
    ];

    // startup call, earnings are added to parents,
    // unless order is from Guest
    public function recordEarning(array $amounts)
    {
        // as this method is called for the user who just
        // made a purchase, this user is required to have rank
        if (! $this->rank) {
            throw new \ErrorException('Cannot record earning for an inactive user');
        }

        $initialAmount = 0;

        if (! $this->exists && array_key_exists($this->rank, $amounts)) {
            // a guest record is not saved
            foreach ($amounts as $rank => $amount) {
                $initialAmount += $amount;

                if ($rank === $this->rank) {
                    // stop accumulating after reaching the guest's rank
                    break;
                }
            }
        }

        $this->recordEarningToParent($this->rank, $amounts, $initialAmount);
    }

    protected function recordEarningToParent($currentRank, array $amounts, $carry)
    {
        $parentRank = $this->parentRank($currentRank);

        if (! $parentRank) {
            return;
        }

        $parent = $this->parent;

        if (! $parent) {
            return;
        }

        $amount = $amounts[$parentRank] + $carry;

        if (($parent->rank === $parentRank) && ($amount > 0)) {
            $earnings = new Earning();
            $earnings->user_id = $parent->getKey();
            $earnings->amount = $amount;
            $earnings->save();

            // as the amount was recorded, there
            // is nothing to carry to the next parent
            $amount = 0;
        }

        // recursive call to the next parent
        $parent->recordEarningToParent($parentRank, $amounts, $amount);
    }

    // find the parent rank, as nullable rank means inactive user
    protected function parentRank($currentRank)
    {
        $currentRankIndex = array_search($currentRank, self::RANKS);

        return self::RANKS[$currentRankIndex + 1] ?? null;
    }

    // relation definition, for reference
    public function parent()
    {
        return $this->belongsTo(User::class, 'parent_id');
    }

    public static function makeGuest($parentId)
    {
        $parent = User::query()->findOrFail($parentId);

        // find the first parent with a rank
        while (! $parent->rank) {
            $parent = $parent->parent;

            if (! $parent) {
                throw new \ErrorException('Could not find an active sponsor for this guest');
            }
        }

        $count = count(self::RANKS);

        $rank = 'Guest';
        for ($i = 0; $i < $count - 1; $i++) {
            if (self::RANKS[$i + 1] === $parent->rank) {
                $rank = self::RANKS[$i];
                break;
            }
        }

        $guest = new User();
        $guest->rank = $rank;
        $guest->parent_id = $parent->id;
        $guest->parent()->associate($parent);

        return $guest;
    }
}

Running the insertion of Earnings

<?php

// ./routes/web.php

use App\Models\Earning;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    // new order from Guest
    $order = new Order();
    $order->sponsor_id = 5; // jade with rank = Basic

    if ($order->user_id > 0) {
        $user = User::query()->findOrFail($order->user_id);
    } elseif ($order->sponsor_id > 0) {
        $user = User::makeGuest($order->sponsor_id);
    }

    if ($user) {
        // Inform the amounts as an array
        $user->recordEarning([
            'Basic' => 50,
            'Junior' => 100,
            'Premium' => 150,
            'Advanced' => 200,
            'Senior' => 250,
        ]);
    }

    return Earning::query()
        // showing of earnings
        ->where('updated_at', '>', now()->subSecond())
        ->get();
});

Adding the amount to parent if the chain is not in order

If this is the scenario:

+--+---------+--------+----+
|id|parent_id|rank    |name|
+--+---------+--------+----+
|1 |NULL     |Senior  |john|
|2 |1        |Advanced|jane|
|3 |2        |Junior  |josh|
|4 |3        |Basic   |joey|
|5 |4        |Basic   |jade|
+--+---------+--------+----+
  1. Jade sent a link to a person and that person order without logging in (Guest)
  2. After making an order the earnings will insert to the parents with rank in chain of Basic -> Junior -> Premium -> Advanced -> Senior
  3. But in that case the chain is not in order so the earnings will insert like this:
Jade = 50
Jane = 450
John = 250
  1. This happened because Joey rank should be Junior and Josh rank should be Premium so it skipped Junior and Premium, therefore
  2. the amount that should be inserted to Joey and Josh is added to the amount Jane will received, Jane received a total of 450 because 100 is from rank Junior (Joey) and 150 is from rank Premium (Josh)

The above code is working fine if the above are the scenarios

But if the scenario below; the amount is not inserting because the records in database is not complete or some parents doesn't exist, and the current code is expecting some parents to skip.

Problem Scenario #1

+--+---------+--------+----+
|id|parent_id|rank    |name|
+--+---------+--------+----+
|1 |NULL     |Senior  |john|
|2 |1        |Basic   |jade|
+--+---------+--------+----+
  1. Jade sent a link to a person and that person order without logging in (Guest)
  2. After making an order the earnings will insert to the parents with rank in chain of Basic -> Junior -> Premium -> Advanced -> Senior
  3. But in that case the it's Basic -> Senior, it skips Junior, Premium, Advanced so the amount for them should be inserted to Senior (John)

Problem Scenario #2

+--+---------+--------+----+
|id|parent_id|rank    |name|
+--+---------+--------+----+
|1 |NULL     |Senior  |john|
|2 |1        |Advanced|jane|
|3 |2        |Advanced|josh|
|4 |3        |Basic   |joey|
|5 |4        |Basic   |jade|
+--+---------+--------+----+
  1. Jade sent a link to a person and that person order without logging in (Guest)
  2. After making an order the earnings will insert to the parents with rank in chain of Basic -> Junior -> Premium -> Advanced -> Senior
  3. But in that case the it's Basic -> Basic -> Advanced, it skips Premium so the amount for them should be inserted to Advanced (Josh), But
  4. what's happening is, it also skips Josh because in the code it's expecting Josh rank is Premium.
  5. The amount of 450 is inserted to Jane which is wrong because there is Advanced rank before Jane, therefore Josh should received the 450 instead of Jane
  6. Jane should be ignored here because there's a first Advanced rank before here.

Amounts inserted on the current code if that is the Scenario

Jade = 50
Jane = 450
John = 250

Amounts expecting to insert

Jade = 50
Josh = 450
John = 250
0 likes
6 replies
NoLAstNamE's avatar

I'm hoping somebody will help me with this.. Thank you.

NoLAstNamE's avatar

Good evening guys, somebody want to help me with my problem.. please guys.

NoLAstNamE's avatar

Hello everyone, please somebody wants to help? I still haven't solve my problem..

Please or to participate in this conversation.