Passed
Push — main ( 6510e5...eb2c28 )
by Thierry
06:37
created

ProfitService::gcd()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 1
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Siak\Tontine\Service\Meeting\Saving;
4
5
use Illuminate\Support\Collection;
6
use Illuminate\Support\Facades\Log;
7
use Siak\Tontine\Model\Fund;
8
use Siak\Tontine\Model\Saving;
9
use Siak\Tontine\Model\Session;
10
use Siak\Tontine\Service\BalanceCalculator;
11
use Siak\Tontine\Service\TenantService;
12
use Siak\Tontine\Service\Tontine\FundService;
13
14
use function gmp_gcd;
15
16
class ProfitService
17
{
18
    /**
19
     * @param BalanceCalculator $balanceCalculator
20
     * @param TenantService $tenantService
21
     * @param FundService $fundService
22
     */
23
    public function __construct(private BalanceCalculator $balanceCalculator,
24
        private TenantService $tenantService, private FundService $fundService)
25
    {}
26
27
    /**
28
     * @param Collection $sessions
29
     * @param Saving $saving
30
     *
31
     * @return int
32
     */
33
    private function getSavingDuration(Collection $sessions, Saving $saving): int
34
    {
35
        // Count the number of sessions before the current one.
36
        return $sessions
37
            ->filter(fn($session) => $session->start_at > $saving->session->start_at)
38
            ->count();
39
    }
40
41
    /**
42
     * @param Collection $values
43
     *
44
     * @return int
45
     */
46
    private function gcd(Collection $values): int
47
    {
48
        return (int)$values->reduce(function($gcd, $value) {
49
            if($gcd === 0)
50
            {
51
                return $value;
52
            }
53
            return gmp_gcd($gcd, $value);
54
        }, $values->first());
55
    }
56
57
    /**
58
     * Get the amount corresponding to one part for a given distribution
59
     *
60
     * @param Distribution $distribution
61
     *
62
     * @return void
63
     */
64
    private function setDistributions(Distribution $distribution): void
65
    {
66
        $sessions = $distribution->sessions;
67
        $savings = $distribution->savings;
68
        // Set savings durations and distributions
69
        foreach($savings as $saving)
70
        {
71
            $saving->duration = $this->getSavingDuration($sessions, $saving);
72
            $saving->parts = $saving->amount * $saving->duration;
73
            $saving->profit = 0;
74
            $saving->percent = 0;
75
        }
76
77
        $distribution->selected = $savings->filter(fn($saving) => $saving->duration > 0);
78
        // The value of the unit part is the gcd of the saving amounts.
79
        // The distribution values might be optimized by using the saving parts value
80
        // instead of the amount, but the resulting part amount might be confusing
81
        // since it can be greater than some saving amounts in certain cases.
82
        $partsGcd = $this->gcd($distribution->selected->pluck('parts'));
83
        if($partsGcd > 0)
84
        {
85
            $amountGcd = $this->gcd($distribution->selected->pluck('amount'));
86
            $distribution->partAmount = $amountGcd;
87
88
            $savings->each(function($saving) use($partsGcd, $amountGcd) {
89
                $saving->profit = $saving->parts / $partsGcd;
90
                $saving->parts /= $amountGcd;
91
            });
92
93
            $profitSum = $savings->sum('profit');
94
            $profitAmount = $distribution->profitAmount;
95
            $savings->each(function($saving) use($profitSum, $profitAmount) {
96
                $percent = $saving->profit / $profitSum;
97
                $saving->percent = $percent * 100;
98
                $saving->profit = (int)($profitAmount * $percent);
99
            });
100
        }
101
    }
102
103
    /**
104
     * Get the profit distribution for savings.
105
     *
106
     * @param Session $session
107
     * @param Fund $fund
108
     * @param int $profitAmount
109
     *
110
     * @return Distribution
111
     */
112
    public function getDistribution(Session $session, Fund $fund, int $profitAmount): Distribution
113
    {
114
        $sessions = $this->fundService->getFundSessions($session, $fund);
115
        // Get the savings to be rewarded
116
        $savings = $fund->savings()
117
            ->select('savings.*')
118
            ->join('members', 'members.id', '=', 'savings.member_id')
119
            ->join('sessions', 'sessions.id', '=', 'savings.session_id')
120
            ->whereIn('sessions.id', $sessions->pluck('id'))
121
            ->orderBy('members.name', 'asc')
122
            ->orderBy('sessions.start_at', 'asc')
123
            ->with(['session', 'member'])
124
            ->get();
125
126
        $distribution = new Distribution($sessions, $savings, $profitAmount);
127
        if($savings->count() === 0)
128
        {
129
            return $distribution;
130
        }
131
132
        // Reduce the distributions by the value of the unit part.
133
        $this->setDistributions($distribution);
134
        return $distribution;
135
    }
136
137
    /**
138
     * Get the total saving and profit amounts.
139
     *
140
     * @param Session $session
141
     * @param Fund $fund
142
     *
143
     * @return array<int>
144
     */
145
    public function getSavingAmounts(Session $session, Fund $fund): array
146
    {
147
        // Get the ids of all the sessions until the current one.
148
        $sessionIds = $this->fundService->getFundSessionIds($session, $fund);
149
        return [
150
            'saving' => $this->balanceCalculator->getSavingsAmount($sessionIds, $fund),
151
            'refund' => $this->balanceCalculator->getRefundsAmount($sessionIds, $fund) +
152
                $this->balanceCalculator->getPartialRefundsAmount($sessionIds, $fund),
153
        ];
154
    }
155
}
156