Passed
Push — main ( 1074e2...00bd25 )
by Thierry
05:52
created

DebtCalculator::getDebtAmount()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 5
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 10
rs 9.6111
1
<?php
2
3
namespace Siak\Tontine\Service\Meeting\Credit;
4
5
use Closure;
6
use Illuminate\Support\Collection;
7
use Siak\Tontine\Model\Debt;
8
use Siak\Tontine\Model\PartialRefund;
9
use Siak\Tontine\Model\Refund;
10
use Siak\Tontine\Model\Session;
11
use Siak\Tontine\Service\TenantService;
12
13
use function pow;
14
15
class DebtCalculator
16
{
17
    /**
18
     * @param TenantService $tenantService
19
     */
20
    public function __construct(protected TenantService $tenantService)
21
    {}
22
23
    /**
24
     * Count the sessions.
25
     *
26
     * @param Session $fromSession The session to start from
27
     * @param Session $toSession The session to end to
28
     *
29
     * @return int
30
     */
31
    private function getSessionCount(Session $fromSession, Session $toSession): int
32
    {
33
        return $this->tenantService->tontine()->sessions()
34
            ->whereDate('start_at', '>', $fromSession->start_at->format('Y-m-d'))
35
            ->whereDate('start_at', '<=', $toSession->start_at->format('Y-m-d'))
36
            ->count();
37
    }
38
39
    /**
40
     * Get the last session for interest calculation
41
     *
42
     * @param Debt $debt
43
     * @param Session $currentSession
44
     *
45
     * @return Session
46
     */
47
    private function getLastSession(Debt $debt, Session $currentSession): Session
48
    {
49
        return $debt->refund &&
50
            $debt->refund->session->start_at < $currentSession->start_at ?
51
            $debt->refund->session : $currentSession;
52
    }
53
54
    /**
55
     * @param Session $current
56
     * @param bool $withCurrent Also take the refunds in the current session.
57
     *
58
     * @return Closure
59
     */
60
    private function getRefundFilter(Session $current, bool $withCurrent): Closure
61
    {
62
        return $withCurrent ?
63
            function(PartialRefund|Refund $refund) use($current) {
64
                return $refund->session->start_at <= $current->start_at;
65
            } :
66
            function(PartialRefund|Refund $refund) use($current) {
67
                return $refund->session->start_at < $current->start_at;
68
            };
69
    }
70
71
    /**
72
     * @param Debt $debt
73
     * @param Session $current
74
     * @param bool $withCurrent Also take the refunds in the current session.
75
     *
76
     * @return Collection
77
     */
78
    private function getPartialRefunds(Debt $debt, Session $current, bool $withCurrent): Collection
79
    {
80
        return $debt->partial_refunds->filter($this->getRefundFilter($current, $withCurrent));
81
    }
82
83
    /**
84
     * @param Debt $debt
85
     * @param Session $current
86
     *
87
     * @return Session
88
     */
89
    private function getLastSessionForInterest(Debt $debt, Session $current): Session
90
    {
91
        // We use a join instead of a subquery so we can order the results by session date.
92
        $closing = $debt->loan->fund->closings()->interest()
93
            ->select('closings.*')
94
            ->join('sessions', 'sessions.id', '=', 'closings.session_id')
95
            ->where('sessions.start_at', '>=', $debt->loan->session->start_at)
96
            ->where('sessions.start_at', '<', $current->start_at)
97
            ->orderBy('sessions.start_at', 'desc')
98
            ->first();
99
        return $closing !== null ? $closing->session : $current;
100
    }
101
102
    /**
103
     * Get the simple interest amount.
104
     *
105
     * @param Debt $debt
106
     * @param Session $session
107
     *
108
     * @return int
109
     */
110
    private function getSimpleInterestAmount(Debt $debt, Session $session): int
111
    {
112
        $principalDebt = $debt->loan->principal_debt;
113
        $loanAmount = $principalDebt->amount;
114
        // The interest rate is multiplied by 100 in the database.
115
        $interestRate = $debt->loan->interest_rate / 10000;
116
        $interestAmount = 0;
117
118
        $startSession = $debt->loan->session;
119
        $endSession = $this->getLastSessionForInterest($debt, $session);
120
121
        // Take refunds before the end session and sort by session date.
122
        $partialRefunds = $this->getPartialRefunds($principalDebt, $endSession, false)
123
            ->sortBy('session.start_at');
124
        foreach($partialRefunds as $refund)
125
        {
126
            $sessionCount = $this->getSessionCount($startSession, $refund->session);
127
            $interestAmount += (int)($loanAmount * $interestRate * $sessionCount);
128
            // For the next loop
129
            $loanAmount -= $refund->amount;
130
            $startSession = $refund->session;
131
        }
132
133
        $lastSession = $this->getLastSession($principalDebt, $endSession);
134
        $sessionCount = $this->getSessionCount($startSession, $lastSession);
135
136
        return $interestAmount + (int)($loanAmount * $interestRate * $sessionCount);
137
    }
138
139
    /**
140
     * Get the compound interest amount.
141
     *
142
     * @param Debt $debt
143
     * @param Session $session
144
     *
145
     * @return int
146
     */
147
    private function getCompoundInterestAmount(Debt $debt, Session $session): int
148
    {
149
        $principalDebt = $debt->loan->principal_debt;
150
        $loanAmount = $principalDebt->amount;
151
        // The interest rate is multiplied by 100 in the database.
152
        $interestRate = $debt->loan->interest_rate / 10000;
153
        $interestAmount = 0;
154
155
        $startSession = $debt->loan->session;
156
        $endSession = $this->getLastSessionForInterest($debt, $session);
157
158
        // Take refunds before the current session and sort by session date.
159
        $partialRefunds = $this->getPartialRefunds($principalDebt, $endSession, false)
160
            ->sortBy('session.start_at');
161
        foreach($partialRefunds as $refund)
162
        {
163
            $sessionCount = $this->getSessionCount($startSession, $refund->session);
164
            $interestAmount += (int)($loanAmount * (pow(1 + $interestRate, $sessionCount) - 1));
165
            // For the next loop
166
            $loanAmount -= $refund->amount - $interestAmount;
167
            $startSession = $refund->session;
168
        }
169
170
        $lastSession = $this->getLastSession($principalDebt, $endSession);
171
        $sessionCount = $this->getSessionCount($startSession, $lastSession);
172
173
        return $interestAmount + (int)($loanAmount * (pow(1 + $interestRate, $sessionCount) - 1));
174
    }
175
176
    /**
177
     * Get the amount of a given debt.
178
     *
179
     * @param Debt $debt
180
     * @param Session $session
181
     *
182
     * @return int
183
     */
184
    public function getDebtAmount(Debt $debt, Session $session): int
185
    {
186
        if($debt->is_principal || $debt->refund || $debt->loan->fixed_interest)
187
        {
188
            return $debt->amount;
189
        }
190
191
        return $debt->loan->simple_interest ?
192
            $this->getSimpleInterestAmount($debt, $session) :
193
            $this->getCompoundInterestAmount($debt, $session);
194
    }
195
196
    /**
197
     * Get the paid amount for a given debt at a given session.
198
     *
199
     * @param Debt $debt
200
     * @param Session $session
201
     *
202
     * @return int
203
     */
204
    public function getDebtPaidAmount(Debt $debt, Session $session): int
205
    {
206
        return $debt->refund !== null ? $debt->amount :
207
            $this->getPartialRefunds($debt, $session, true)->sum('amount');
208
    }
209
210
    /**
211
     * Get the unpaid amount for a given debt at a given session.
212
     *
213
     * @param Debt $debt
214
     * @param Session $session
215
     *
216
     * @return int
217
     */
218
    public function getDebtUnpaidAmount(Debt $debt, Session $session): int
219
    {
220
        return $this->getDebtAmount($debt, $session) - $this->getDebtPaidAmount($debt, $session);
221
    }
222
223
    /**
224
     * @param Debt $debt
225
     * @param Session $current
226
     *
227
     * @return Collection
228
     */
229
    private function getNextPartialRefunds(Debt $debt, Session $current): Collection
230
    {
231
        // We use a join instead of a subquery so we can order the results by session date.
232
        return $debt->partial_refunds()
233
            ->select('partial_refunds.*')
234
            ->join('sessions', 'sessions.id', '=', 'partial_refunds.session_id')
235
            ->where('sessions.start_at', '>', $current->start_at)
236
            ->orderBy('sessions.start_at', 'desc')
237
            ->get();
238
    }
239
240
    /**
241
     * Get the max amount that can be paid for a given debt at a given session.
242
     *
243
     * @param Debt $debt
244
     * @param Session $session
245
     *
246
     * @return int
247
     */
248
    public function getDebtPayableAmount(Debt $debt, Session $session): int
249
    {
250
        if($debt->refund !== null)
251
        {
252
            return 0;
253
        }
254
255
        $partialRefundAmount = $debt->partial_refunds()->sum('amount');
256
        if($debt->is_principal || $debt->loan->fixed_interest)
257
        {
258
            return $debt->amount - $partialRefundAmount;
259
        }
260
261
        // For debts with simple or compound interest, the payable amount calculation
262
        // must take into account any partial refund after the current session.
263
        $currentDebtAmount = $this->getDebtAmount($debt, $session);
264
        $nextPartialRefunds = $this->getNextPartialRefunds($debt, $session);
265
        if($nextPartialRefunds->count() === 0)
266
        {
267
            return $currentDebtAmount - $partialRefundAmount;
268
        }
269
270
        $lastSession = $nextPartialRefunds[0]->session;
271
        $addedDebt = $this->getDebtAmount($debt, $lastSession) - $currentDebtAmount;
272
        $addedRefund = $nextPartialRefunds->sum('amount');
273
274
        return $addedRefund < $addedDebt ?
275
            $currentDebtAmount - $partialRefundAmount :
276
            $currentDebtAmount - $partialRefundAmount - $addedRefund + $addedDebt;
277
    }
278
279
    /**
280
     * Get the amount due of a given debt before the given session.
281
     *
282
     * @param Debt $debt
283
     * @param Session $session
284
     * @param bool $withCurrent Take the current session into account.
285
     *
286
     * @return int
287
     */
288
    public function getDebtDueAmount(Debt $debt, Session $session, bool $withCurrent): int
289
    {
290
        $refundFilter = $this->getRefundFilter($session, $withCurrent);
291
        if($debt->refund !== null && $refundFilter($debt->refund))
292
        {
293
            return 0; // The debt was refunded before the current session.
294
        }
295
296
        return $this->getDebtAmount($debt, $session) -
297
            $this->getPartialRefunds($debt, $session, $withCurrent)->sum('amount');
298
    }
299
}
300