Passed
Push — main ( ccbeb2...45ea91 )
by Thierry
05:34
created

ProfitService::getProfitAmount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
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
     * @param Session $currentSession
30
     * @param int $fundId
31
     *
32
     * @return mixed
33
     */
34
    private function getFundSessionsQuery(Session $currentSession, int $fundId)
35
    {
36
        $lastSessionDate = $currentSession->start_at->format('Y-m-d');
37
        // The closing sessions ids
38
        $closingSessionIds = array_keys($this->savingService->getFundClosings($fundId));
39
        if(count($closingSessionIds) === 0)
40
        {
41
            // No closing session yet
42
            return $this->tenantService->tontine()->sessions()
43
                ->whereDate('sessions.start_at', '<=', $lastSessionDate);
44
        }
45
        // The previous closing sessions
46
        $closingSessions = $this->tenantService->tontine()->sessions()
47
            ->where('sessions.id', '!=', $currentSession->id)
48
            ->whereIn('sessions.id', $closingSessionIds)
49
            ->whereDate('sessions.start_at', '<', $lastSessionDate)
50
            ->orderByDesc('sessions.start_at')
51
            ->get();
52
        if($closingSessions->count() === 0)
53
        {
54
            // All the closing sessions are after the current session.
55
            return $this->tenantService->tontine()->sessions()
56
                ->whereDate('sessions.start_at', '<=', $lastSessionDate);
57
        }
58
59
        // The most recent previous closing session
60
        $firstSessionDate = $closingSessions->last()->start_at->format('Y-m-d');
61
        // Return all the sessions after the most recent previous closing session
62
        return $this->tenantService->tontine()->sessions()
63
            ->whereDate('sessions.start_at', '<=', $lastSessionDate)
64
            ->whereDate('sessions.start_at', '>', $firstSessionDate);
65
    }
66
67
    /**
68
     * Get the sessions to be used for profit calculation.
69
     *
70
     * @param Session $currentSession
71
     * @param int $fundId
72
     *
73
     * @return Collection
74
     */
75
    public function getFundSessions(Session $currentSession, int $fundId): Collection
76
    {
77
        return $this->getFundSessionsQuery($currentSession, $fundId)->get();
78
    }
79
80
    /**
81
     * Get the id of sessions to be used for profit calculation.
82
     *
83
     * @param Session $currentSession
84
     * @param int $fundId
85
     *
86
     * @return Collection
87
     */
88
    public function getFundSessionIds(Session $currentSession, int $fundId): Collection
89
    {
90
        return $this->getFundSessionsQuery($currentSession, $fundId)->pluck('id');
91
    }
92
93
    /**
94
     * @param Collection $sessions
95
     * @param Saving $saving
96
     *
97
     * @return int
98
     */
99
    private function getSavingDuration(Collection $sessions, Saving $saving): int
100
    {
101
        // Count the number of sessions before the current one.
102
        return $sessions->filter(function($session) use($saving) {
103
            return $session->start_at > $saving->session->start_at;
104
        })->count();
105
    }
106
107
    /**
108
     * Get the profit distribution for savings.
109
     *
110
     * @param Collection $sessions
111
     * @param Collection $savings
112
     * @param int $profitAmount
113
     *
114
     * @return Collection
115
     */
116
    private function setDistributions(Collection $sessions, Collection $savings,
117
        int $profitAmount): Collection
118
    {
119
        // Set savings durations and distributions
120
        foreach($savings as $saving)
121
        {
122
            $saving->duration = $this->getSavingDuration($sessions, $saving);
123
            $saving->distribution = $saving->amount * $saving->duration;
124
            $saving->profit = 0;
125
        }
126
        // Reduce the distributions
127
        $distributionGcd = (int)$savings->reduce(function($gcd, $saving) {
128
            if($gcd === 0)
129
            {
130
                return $saving->distribution;
131
            }
132
            if($saving->duration === 0)
133
            {
134
                return $gcd;
135
            }
136
            return gmp_gcd($gcd, $saving->distribution);
137
        }, $savings->first()->distribution);
138
        if($distributionGcd > 0)
139
        {
140
            $sum = (int)($savings->sum('distribution') / $distributionGcd);
141
            foreach($savings as $saving)
142
            {
143
                $saving->distribution /= $distributionGcd;
144
                $saving->profit = (int)($profitAmount * $saving->distribution / $sum);
145
            }
146
        }
147
148
        return $savings;
149
    }
150
151
    /**
152
     * Get the profit distribution for savings.
153
     *
154
     * @param Session $session
155
     * @param int $fundId
156
     * @param int $profitAmount
157
     *
158
     * @return Collection
159
     */
160
    public function getDistributions(Session $session, int $fundId, int $profitAmount): Collection
161
    {
162
        $sessions = $this->getFundSessions($session, $fundId);
163
        // Get the savings to be rewarded
164
        $query = Saving::select('savings.*')
165
            ->join('members', 'members.id', '=', 'savings.member_id')
166
            ->join('sessions', 'sessions.id', '=', 'savings.session_id')
167
            ->whereIn('sessions.id', $sessions->pluck('id'))
168
            ->orderBy('members.name', 'asc')
169
            ->orderBy('sessions.start_at', 'asc')
170
            ->with(['session', 'member']);
171
        $savings = $fundId > 0 ?
172
            $query->where('savings.fund_id', $fundId)->get() :
173
            $query->whereNull('savings.fund_id')->get();
174
        if($savings->count() === 0)
175
        {
176
            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...
177
        }
178
179
        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

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