Passed
Push — main ( 936852...10149e )
by Thierry
15:09
created

ProfitService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 2
dl 0
loc 3
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\DB;
7
use Illuminate\Support\Facades\Log;
8
use Siak\Tontine\Model\Debt;
9
use Siak\Tontine\Model\Fund;
10
use Siak\Tontine\Model\Saving;
11
use Siak\Tontine\Model\Session;
12
use Siak\Tontine\Service\TenantService;
13
14
use function array_keys;
15
use function count;
16
use function gmp_gcd;
17
18
class ProfitService
19
{
20
    /**
21
     * @param TenantService $tenantService
22
     * @param SavingService $savingService
23
     */
24
    public function __construct(protected TenantService $tenantService,
25
        protected SavingService $savingService)
26
    {}
27
28
    /**
29
     * Get the sessions to be used for profit calculation.
30
     *
31
     * @param Session $currentSession
32
     * @param int $fundId
33
     *
34
     * @return Collection
35
     */
36
    public function getFundSessions(Session $currentSession, int $fundId): Collection
37
    {
38
        $lastSessionDate = $currentSession->start_at->format('Y-m-d');
39
        // The closing sessions ids
40
        $closingSessionIds = array_keys($this->savingService->getFundClosings($fundId));
41
        if(count($closingSessionIds) === 0)
42
        {
43
            // No closing session yet
44
            return $this->tenantService->tontine()->sessions()
45
                ->whereDate('sessions.start_at', '<=', $lastSessionDate)->get();
46
        }
47
        // The previous closing sessions
48
        $closingSessions = $this->tenantService->tontine()->sessions()
49
            ->where('sessions.id', '!=', $currentSession->id)
50
            ->whereIn('sessions.id', $closingSessionIds)
51
            ->whereDate('sessions.start_at', '<', $lastSessionDate)
52
            ->orderByDesc('sessions.start_at')
53
            ->get();
54
        if($closingSessions->count() === 0)
55
        {
56
            // All the closing sessions are after the current session.
57
            return $this->tenantService->tontine()->sessions()
58
                ->whereDate('sessions.start_at', '<=', $lastSessionDate)->get();
59
        }
60
61
        // The most recent previous closing session
62
        $firstSessionDate = $closingSessions->last()->start_at->format('Y-m-d');
63
        // Return all the sessions after the most recent previous closing session
64
        return $this->tenantService->tontine()->sessions()
65
            ->whereDate('sessions.start_at', '<=', $lastSessionDate)
66
            ->whereDate('sessions.start_at', '>', $firstSessionDate)
67
            ->get();
68
    }
69
70
    /**
71
     * @param Collection $sessions
72
     * @param Saving $saving
73
     *
74
     * @return int
75
     */
76
    private function getSavingDuration(Collection $sessions, Saving $saving): int
77
    {
78
        // Count the number of sessions before the current one.
79
        return $sessions->filter(function($session) use($saving) {
80
            return $session->start_at > $saving->session->start_at;
81
        })->count();
82
    }
83
84
    /**
85
     * Get the profit distribution for savings.
86
     *
87
     * @param Collection $sessions
88
     * @param Collection $savings
89
     * @param int $profitAmount
90
     *
91
     * @return Collection
92
     */
93
    private function setDistributions(Collection $sessions, Collection $savings,
94
        int $profitAmount): Collection
95
    {
96
        // Set savings durations and distributions
97
        foreach($savings as $saving)
98
        {
99
            $saving->duration = $this->getSavingDuration($sessions, $saving);
100
            $saving->distribution = $saving->amount * $saving->duration;
101
            $saving->profit = 0;
102
        }
103
        // Reduce the distributions
104
        $distributionGcd = (int)$savings->reduce(function($gcd, $saving) {
105
            if($gcd === 0)
106
            {
107
                return $saving->distribution;
108
            }
109
            if($saving->duration === 0)
110
            {
111
                return $gcd;
112
            }
113
            return gmp_gcd($gcd, $saving->distribution);
114
        }, $savings->first()->distribution);
115
        if($distributionGcd > 0)
116
        {
117
            $sum = (int)($savings->sum('distribution') / $distributionGcd);
118
            foreach($savings as $saving)
119
            {
120
                $saving->distribution /= $distributionGcd;
121
                $saving->profit = (int)($profitAmount * $saving->distribution / $sum);
122
            }
123
        }
124
125
        return $savings;
126
    }
127
128
    /**
129
     * Get the profit distribution for savings.
130
     *
131
     * @param Session $session
132
     * @param int $fundId
133
     * @param int $profitAmount
134
     *
135
     * @return Collection
136
     */
137
    public function getDistributions(Session $session, int $fundId, int $profitAmount): Collection
138
    {
139
        $sessions = $this->getFundSessions($session, $fundId);
140
        // Get the savings to be rewarded
141
        $query = Saving::select('savings.*')
142
            ->join('members', 'members.id', '=', 'savings.member_id')
143
            ->join('sessions', 'sessions.id', '=', 'savings.session_id')
144
            ->whereIn('sessions.id', $sessions->pluck('id'))
145
            ->orderBy('members.name', 'asc')
146
            ->orderBy('sessions.start_at', 'asc')
147
            ->with(['session', 'member']);
148
        $savings = $fundId > 0 ?
149
            $query->where('savings.fund_id', $fundId)->get() :
150
            $query->whereNull('savings.fund_id')->get();
151
        if($savings->count() === 0)
152
        {
153
            return $savings;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $savings could return the type Illuminate\Database\Eloq...Relations\HasOneThrough which is incompatible with the type-hinted return Illuminate\Support\Collection. Consider adding an additional type-check to rule them out.
Loading history...
154
        }
155
156
        return $this->setDistributions($sessions, $savings, $profitAmount);
0 ignored issues
show
Bug introduced by
It seems like $savings can also be of type Illuminate\Database\Eloq...elations\HasManyThrough and Illuminate\Database\Eloq...Relations\HasOneThrough; however, parameter $savings of Siak\Tontine\Service\Mee...ice::setDistributions() does only seem to accept Illuminate\Support\Collection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
        return $this->setDistributions($sessions, /** @scrutinizer ignore-type */ $savings, $profitAmount);
Loading history...
157
    }
158
159
    /**
160
     * Get the amount corresponding to one part for a given distribution
161
     *
162
     * @param Collection $savings
163
     *
164
     * @return int
165
     */
166
    public function getPartUnitValue(Collection $savings): int
167
    {
168
        // The part value makes sense only iwhen there is more than 2 savings
169
        // with distribution greater than 0.
170
        $savings = $savings->filter(fn($saving) => $saving->distribution > 0);
171
        if($savings->count() < 2)
172
        {
173
            return 0;
174
        }
175
176
        $saving = $savings->first();
177
        return (int)($saving->amount * $saving->duration / $saving->distribution);
178
    }
179
180
    /**
181
     * @param int $fundId
182
     *
183
     * @return Fund|null
184
     */
185
    public function getFund(int $fundId): ?Fund
186
    {
187
        return $this->tenantService->tontine()->funds()->find($fundId);
188
    }
189
190
    /**
191
     * Get the sum of savings amounts.
192
     *
193
     * @param Collection $sessionId
194
     * @param int $fundId
195
     *
196
     * @return int
197
     */
198
    private function getSavingAmount(Collection $sessionIds, int $fundId): int
199
    {
200
        $query = DB::table('savings')
201
            ->select(DB::raw("sum(amount) as total"))
202
            ->whereIn('session_id', $sessionIds);
203
        $saving = $fundId > 0 ?
204
            $query->where('savings.fund_id', $fundId)->first() :
205
            $query->whereNull('savings.fund_id')->first();
206
        return $saving->total ?? 0;
207
    }
208
209
    /**
210
     * Get the sum of refunded interests.
211
     *
212
     * @param Collection $sessionId
213
     * @param int $fundId
214
     *
215
     * @return int
216
     */
217
    private function getRefundAmount(Collection $sessionIds, int $fundId): int
218
    {
219
        $query = DB::table('refunds')
220
            ->join('debts', 'refunds.debt_id', '=', 'debts.id')
221
            ->join('loans', 'debts.loan_id', '=', 'loans.id')
222
            ->select(DB::raw("sum(debts.amount) as total"))
223
            ->where('debts.type', Debt::TYPE_INTEREST)
224
            ->whereIn('refunds.session_id', $sessionIds);
225
        $refund = $fundId > 0 ?
226
            $query->where('loans.fund_id', $fundId)->first() :
227
            $query->whereNull('loans.fund_id')->first();
228
        return $refund->total ?? 0;
229
    }
230
231
    /**
232
     * Get the total saving and profit amounts.
233
     *
234
     * @param Session $session
235
     * @param int $fundId
236
     *
237
     * @return array<int>
238
     */
239
    public function getSavingAmounts(Session $session, int $fundId): array
240
    {
241
        // Get the ids of all the sessions until the current one.
242
        $sessionIds = $this->getFundSessions($session, $fundId)->pluck('id');
243
        return [
244
            'saving' => $this->getSavingAmount($sessionIds, $fundId),
245
            'refund' => $this->getRefundAmount($sessionIds, $fundId),
246
        ];
247
    }
248
249
    /**
250
     * Get the profit amount saved on this session.
251
     *
252
     * @param Session $session
253
     * @param int $fundId
254
     *
255
     * @return int
256
     */
257
    public function getProfitAmount(Session $session, int $fundId): int
258
    {
259
        return $this->savingService->getProfitAmount($session, $fundId);
260
    }
261
262
    /**
263
     * Check if the given session is closing the fund.
264
     *
265
     * @param Session $session
266
     * @param int $fundId
267
     *
268
     * @return bool
269
     */
270
    public function hasFundClosing(Session $session, int $fundId): bool
271
    {
272
        return $this->savingService->hasFundClosing($session, $fundId);
273
    }
274
}
275