BalanceCalculator   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 127
c 1
b 0
f 0
dl 0
loc 371
rs 10
wmc 28

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getReceivableAmount() 0 8 3
A __construct() 0 2 1
A getDepositQuery() 0 8 1
A getRemitmentQuery() 0 8 1
A getPoolDepositAmount() 0 8 2
A getFundsAmount() 0 13 1
A getSettlementsAmount() 0 5 1
A getRemitmentsAmount() 0 28 1
A getAuctionsAmount() 0 3 1
A getDepositsAmount() 0 8 1
A getRemitmentAmountSqlValue() 0 3 1
A getPoolLateDepositAmount() 0 8 2
A getTotalBalance() 0 11 1
A getPoolRemitmentAmount() 0 12 2
A getSavingsAmount() 0 5 1
A getPayableAmount() 0 8 2
A getRefundsAmount() 0 6 1
A getOutflowsAmount() 0 10 1
A getBalanceForLoan() 0 11 1
A getBalances() 0 23 1
A getPartialRefundsAmount() 0 10 1
A getLoansAmount() 0 6 1
1
<?php
2
3
namespace Siak\Tontine\Service\Payment;
4
5
use Illuminate\Database\Query\Builder;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Facades\DB;
8
use Siak\Tontine\Model\Auction;
9
use Siak\Tontine\Model\Bill;
10
use Siak\Tontine\Model\Debt;
11
use Siak\Tontine\Model\Outflow;
12
use Siak\Tontine\Model\Fund;
13
use Siak\Tontine\Model\PartialRefund;
14
use Siak\Tontine\Model\Pool;
15
use Siak\Tontine\Model\Receivable;
16
use Siak\Tontine\Model\Session;
17
use Siak\Tontine\Service\Meeting\Saving\FundService;
18
19
class BalanceCalculator
20
{
21
    /**
22
     * @param FundService $fundService
23
     */
24
    public function __construct(private FundService $fundService)
25
    {}
26
27
    /**
28
     * @param Receivable $receivable
29
     *
30
     * @return int
31
     */
32
    public function getReceivableAmount(Receivable $receivable): int
33
    {
34
        if($receivable->subscription->pool->deposit_fixed)
35
        {
36
            return $receivable->subscription->pool->amount;
37
        }
38
39
        return !$receivable->deposit ? 0 : $receivable->deposit->amount;
0 ignored issues
show
Bug introduced by
The property amount does not seem to exist on Siak\Tontine\Model\Deposit. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
40
    }
41
42
    /**
43
     * @param bool $withPoolTable
44
     *
45
     * @return Builder
46
     */
47
    private function getDepositQuery(bool $withPoolTable): Builder
48
    {
49
        return DB::table('deposits')
50
            ->join('receivables', 'deposits.receivable_id', '=', 'receivables.id')
51
            ->join('subscriptions', 'receivables.subscription_id', '=', 'subscriptions.id')
52
            ->when($withPoolTable, function(Builder $query) {
53
                $query->join('pools', 'subscriptions.pool_id', '=', 'pools.id')
54
                    ->join(DB::raw('pool_defs as pd'), 'pools.def_id', '=', 'pd.id');
55
            });
56
    }
57
58
    /**
59
     * @return Builder
60
     */
61
    private function getRemitmentQuery(): Builder
62
    {
63
        return DB::table('remitments')
64
            ->join('payables', 'remitments.payable_id', '=', 'payables.id')
65
            ->join('subscriptions', 'payables.subscription_id', '=', 'subscriptions.id')
66
            ->join('pools', 'subscriptions.pool_id', '=', 'pools.id')
67
            ->join(DB::raw('v_pools as vp'), 'vp.pool_id', '=', 'pools.id')
68
            ->join(DB::raw('pool_defs as pd'), 'pools.def_id', '=', 'pd.id');
69
    }
70
71
    /**
72
     * @param Pool $pool
73
     * @param Session $session
74
     *
75
     * @return int
76
     */
77
    public function getPoolDepositAmount(Pool $pool, Session $session): int
78
    {
79
        $query = $this->getDepositQuery(false)
80
            ->where('subscriptions.pool_id', $pool->id)
81
            // ->where('deposits.session_id', $session->id)
82
            ->where('receivables.session_id', $session->id);
83
        return $pool->deposit_fixed ? $pool->amount * $query->count() :
84
            $query->sum('deposits.amount');
85
    }
86
87
    /**
88
     * @param Pool $pool
89
     * @param Session $session
90
     *
91
     * @return int
92
     */
93
    public function getPoolLateDepositAmount(Pool $pool, Session $session): int
94
    {
95
        $query = $this->getDepositQuery(false)
96
            ->where('subscriptions.pool_id', $pool->id)
97
            ->where('deposits.session_id', $session->id)
98
            ->where('receivables.session_id', '!=', $session->id);
99
        return $pool->deposit_fixed ? $pool->amount * $query->count() :
100
            $query->sum('deposits.amount');
101
    }
102
103
    /**
104
     * @param Pool $pool
105
     * @param Session $session
106
     *
107
     * @return int
108
     */
109
    public function getPayableAmount(Pool $pool, Session $session): int
110
    {
111
        if(!$pool->deposit_fixed)
112
        {
113
            // Sum the amounts for all deposits
114
            return $this->getPoolDepositAmount($pool, $session);
115
        }
116
        return $pool->amount * $pool->sessions()->count();
117
    }
118
119
    /**
120
     * @return string
121
     */
122
    private function getRemitmentAmountSqlValue(): string
123
    {
124
        return 'pd.amount * vp.sessions_count';
125
    }
126
127
    /**
128
     * @param Pool $pool
129
     * @param Session $session
130
     *
131
     * @return int
132
     */
133
    public function getPoolRemitmentAmount(Pool $pool, Session $session): int
134
    {
135
        if(!$pool->deposit_fixed)
136
        {
137
            // Sum the amounts for all deposits
138
            return $this->getPoolDepositAmount($pool, $session);
139
        }
140
141
        return $this->getRemitmentQuery()
142
            ->where('payables.session_id', $session->id)
143
            ->where('subscriptions.pool_id', $pool->id)
144
            ->sum(DB::raw($this->getRemitmentAmountSqlValue()));
145
    }
146
147
    /**
148
     * @param Collection $sessionIds
149
     * @param bool $lendable
150
     *
151
     * @return int
152
     */
153
    private function getDepositsAmount(Collection $sessionIds, bool $lendable)
154
    {
155
        return $this->getDepositQuery(true)
156
            ->whereIn('deposits.session_id', $sessionIds)
157
            ->when($lendable, function(Builder $query) {
158
                $query->where('pd.properties->deposit->lendable', true);
159
            })
160
            ->sum(DB::raw('deposits.amount + pd.amount'));
161
    }
162
163
    /**
164
     * @param Collection $sessionIds
165
     * @param bool $lendable
166
     *
167
     * @return int
168
     */
169
    private function getRemitmentsAmount(Collection $sessionIds, bool $lendable)
170
    {
171
        return
172
            // Remitment sum for pools with fixed deposits.
173
            // Each value is the pool amount multiply by the number od sessions.
174
            $this->getRemitmentQuery()
175
                ->whereIn('payables.session_id', $sessionIds)
176
                ->where('pd.properties->deposit->fixed', true)
177
                ->when($lendable, function(Builder $query) {
178
                    $query->where('pd.properties->deposit->lendable', true);
179
                })
180
                ->sum(DB::raw($this->getRemitmentAmountSqlValue()))
181
            // Remitment sum for pools with libre deposits.
182
            // Each value is the sum of deposits for the given pool.
183
            + $this->getDepositQuery(true)
184
                ->whereIn('deposits.session_id', $sessionIds)
185
                ->whereExists(function(Builder $query) {
186
                    $query->select(DB::raw(1))->from('remitments')
0 ignored issues
show
Bug introduced by
'remitments' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $table of Illuminate\Database\Query\Builder::from(). ( Ignorable by Annotation )

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

186
                    $query->select(DB::raw(1))->from(/** @scrutinizer ignore-type */ 'remitments')
Loading history...
187
                        ->join(DB::raw('payables p'), 'remitments.payable_id', '=', 'p.id')
188
                        ->join(DB::raw('subscriptions s'), 'p.subscription_id', '=', 's.id')
189
                        ->whereColumn('p.session_id', 'deposits.session_id')
190
                        ->whereColumn('s.pool_id', 'pools.id');
191
                })
192
                ->where('pd.properties->deposit->fixed', false)
193
                ->when($lendable, function(Builder $query) {
194
                    $query->where('pd.properties->deposit->lendable', true);
195
                })
196
                ->sum('deposits.amount');
197
    }
198
199
    /**
200
     * @param Collection $sessionIds
201
     *
202
     * @return int
203
     */
204
    private function getAuctionsAmount(Collection $sessionIds)
205
    {
206
        return Auction::paid()->whereIn('session_id', $sessionIds)->sum('amount');
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode...sionIds)->sum('amount') also could return the type Illuminate\Database\Eloq...\Database\Query\Builder which is incompatible with the documented return type integer.
Loading history...
207
    }
208
209
    /**
210
     * @param Collection $sessionIds
211
     * @param bool $lendable
212
     *
213
     * @return int
214
     */
215
    private function getSettlementsAmount(Collection $sessionIds, bool $lendable)
216
    {
217
        return Bill::whereHas('settlement', fn($qs) => $qs->whereIn('session_id', $sessionIds))
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode..... */ })->sum('amount') also could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the documented return type integer.
Loading history...
218
            ->when($lendable, fn($qb) => $qb->lendable(true))
219
            ->sum('amount');
220
    }
221
222
    /**
223
     * @param Collection $sessionIds
224
     * @param bool $lendable
225
     *
226
     * @return int
227
     */
228
    private function getOutflowsAmount(Collection $sessionIds, bool $lendable)
229
    {
230
        return Outflow::whereIn('session_id', $sessionIds)
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode..... */ })->sum('amount') also could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the documented return type integer.
Loading history...
231
            ->when($lendable, function($query) {
232
                $query->where(function($query) {
233
                    $query->whereDoesntHave('charge')
234
                        ->orWhereHas('charge', fn($qb) => $qb->lendable(true));
235
                });
236
            })
237
            ->sum('amount');
238
    }
239
240
    /**
241
     * @param Collection $sessionIds
242
     * @param Fund $fund
243
     *
244
     * @return int
245
     */
246
    public function getSavingsAmount(Collection $sessionIds, Fund $fund)
247
    {
248
        return $fund->savings()
249
            ->whereIn('session_id', $sessionIds)
250
            ->sum('amount');
251
    }
252
253
    /**
254
     * @param Collection $sessionIds
255
     * @param Fund $fund
256
     *
257
     * @return int
258
     */
259
    public function getRefundsAmount(Collection $sessionIds, Fund $fund)
260
    {
261
        return Debt::interest()
262
            ->whereHas('refund', fn($query) => $query->whereIn('session_id', $sessionIds))
263
            ->whereHas('loan', fn($query) => $query->where('fund_id', $fund->id))
264
            ->sum('amount');
265
    }
266
267
    /**
268
     * @param Collection $sessionIds
269
     * @param Fund $fund
270
     *
271
     * @return int
272
     */
273
    public function getPartialRefundsAmount(Collection $sessionIds, Fund $fund)
274
    {
275
        // Filter on debts that are not yet refunded.
276
        return PartialRefund::whereIn('session_id', $sessionIds)
277
            ->whereHas('debt', function($query) use($fund) {
278
                $query->interest()
279
                    ->whereDoesntHave('refund')
280
                    ->whereHas('loan', fn($query) => $query->where('fund_id', $fund->id));
281
            })
282
            ->sum('amount');
283
    }
284
285
    /**
286
     * @param Collection $sessionIds
287
     * @param Fund $fund
288
     *
289
     * @return int
290
     */
291
    private function getLoansAmount(Collection $sessionIds, Fund $fund)
292
    {
293
        return Debt::principal()
294
            ->whereHas('loan', fn($ql) => $ql->where('fund_id', $fund->id)
295
                ->whereIn('session_id', $sessionIds))
296
            ->sum('amount');
297
    }
298
299
    /**
300
     * @param Session $session    The session
301
     *
302
     * @return int
303
     */
304
    private function getFundsAmount(Session $session)
305
    {
306
        // Each fund can have a different set of sessions, so we need to loop on all funds.
307
        return $this->fundService->getSessionFunds($session)
308
            ->reduce(function(int $amount, Fund $fund) use($session) {
309
                $sessionIds = $this->fundService->getFundSessionIds($fund, $session);
310
311
                return $amount
312
                    + $this->getSavingsAmount($sessionIds, $fund)
313
                    + $this->getRefundsAmount($sessionIds, $fund)
314
                    + $this->getPartialRefundsAmount($sessionIds, $fund)
315
                    - $this->getLoansAmount($sessionIds, $fund);
316
            }, 0);
0 ignored issues
show
Bug introduced by
0 of type integer is incompatible with the type Illuminate\Support\Traits\TReduceInitial expected by parameter $initial of Illuminate\Support\Collection::reduce(). ( Ignorable by Annotation )

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

316
            }, /** @scrutinizer ignore-type */ 0);
Loading history...
317
    }
318
319
    /**
320
     * Get the amount available for loan.
321
     *
322
     * @param Session $session    The session
323
     *
324
     * @return int
325
     */
326
    public function getBalanceForLoan(Session $session): int
327
    {
328
        // Get the ids of all the sessions until the current one.
329
        $sessionIds = Session::precedes($session)->pluck('id');
330
331
        return $this->getAuctionsAmount($sessionIds)
332
            + $this->getSettlementsAmount($sessionIds, true)
333
            + $this->getDepositsAmount($sessionIds, true)
334
            - $this->getRemitmentsAmount($sessionIds, true)
335
            - $this->getOutflowsAmount($sessionIds, true)
336
            + $this->getFundsAmount($session);
337
    }
338
339
    /**
340
     * Get the amount available for outflow.
341
     *
342
     * @param Session $session    The session
343
     *
344
     * @return int
345
     */
346
    public function getTotalBalance(Session $session): int
347
    {
348
        // Get the ids of all the sessions until the current one.
349
        $sessionIds = Session::precedes($session)->pluck('id');
350
351
        return $this->getAuctionsAmount($sessionIds)
352
            + $this->getSettlementsAmount($sessionIds, false)
353
            + $this->getDepositsAmount($sessionIds, false)
354
            - $this->getRemitmentsAmount($sessionIds, false)
355
            - $this->getOutflowsAmount($sessionIds, false)
356
            + $this->getFundsAmount($session);
357
    }
358
359
    /**
360
     * Get the detailed amounts.
361
     *
362
     * @param Session $session    The session
363
     * @param bool $lendable
364
     *
365
     * @return array<int>
366
     */
367
    public function getBalances(Session $session, bool $lendable): array
368
    {
369
        $fundAmounts = $this->fundService->getSessionFunds($session)
370
            ->reduce(function(array $amounts, Fund $fund) use($session) {
371
                $sessionIds = $this->fundService->getFundSessionIds($fund, $session);
372
373
                return [
374
                    'savings' => $amounts['savings'] + $this->getSavingsAmount($sessionIds, $fund),
375
                    'loans' => $amounts['loans'] + $this->getLoansAmount($sessionIds, $fund),
376
                    'refunds' => $amounts['refunds'] + $this->getRefundsAmount($sessionIds, $fund) +
377
                        $this->getPartialRefundsAmount($sessionIds, $fund),
378
                ];
379
            }, ['savings' => 0, 'loans' => 0, 'refunds' => 0]);
0 ignored issues
show
Bug introduced by
array('savings' => 0, 'l...' => 0, 'refunds' => 0) of type array<string,integer> is incompatible with the type Illuminate\Support\Traits\TReduceInitial expected by parameter $initial of Illuminate\Support\Collection::reduce(). ( Ignorable by Annotation )

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

379
            }, /** @scrutinizer ignore-type */ ['savings' => 0, 'loans' => 0, 'refunds' => 0]);
Loading history...
380
381
        // Get the ids of all the sessions until the current one.
382
        $sessionIds = Session::precedes($session)->pluck('id');
383
        return [
384
            'auctions' => $this->getAuctionsAmount($sessionIds),
385
            'charges' => $this->getSettlementsAmount($sessionIds, $lendable),
386
            'deposits' => $this->getDepositsAmount($sessionIds, $lendable),
387
            'remitments' => $this->getRemitmentsAmount($sessionIds, $lendable),
388
            'outflows' => $this->getOutflowsAmount($sessionIds, $lendable),
389
            ...$fundAmounts,
390
        ];
391
    }
392
}
393