BalanceCalculator::getBalances()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 17
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 23
rs 9.7
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
        // Don't take transfers from savings funds to settlements into account.
218
        return Bill::whereHas('settlement', fn($qs) =>
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...
219
                $qs->whereNull('fund_id')->whereIn('session_id', $sessionIds))
220
            ->when($lendable, fn($qb) => $qb->lendable(true))
221
            ->sum('amount');
222
    }
223
224
    /**
225
     * @param Collection $sessionIds
226
     * @param bool $lendable
227
     *
228
     * @return int
229
     */
230
    private function getOutflowsAmount(Collection $sessionIds, bool $lendable)
231
    {
232
        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...
233
            ->when($lendable, function($query) {
234
                $query->where(function($query) {
235
                    $query->whereDoesntHave('charge')
236
                        ->orWhereHas('charge', fn($qb) => $qb->lendable(true));
237
                });
238
            })
239
            ->sum('amount');
240
    }
241
242
    /**
243
     * @param Collection $sessionIds
244
     * @param Fund $fund
245
     *
246
     * @return int
247
     */
248
    public function getSavingsAmount(Collection $sessionIds, Fund $fund)
249
    {
250
        return $fund->savings()
251
            ->whereIn('session_id', $sessionIds)
252
            ->sum('amount');
253
    }
254
255
    /**
256
     * @param Collection $sessionIds
257
     * @param Fund $fund
258
     *
259
     * @return int
260
     */
261
    public function getRefundsAmount(Collection $sessionIds, Fund $fund)
262
    {
263
        return Debt::interest()
264
            ->whereHas('refund', fn($query) => $query->whereIn('session_id', $sessionIds))
265
            ->whereHas('loan', fn($query) => $query->where('fund_id', $fund->id))
266
            ->sum('amount');
267
    }
268
269
    /**
270
     * @param Collection $sessionIds
271
     * @param Fund $fund
272
     *
273
     * @return int
274
     */
275
    public function getPartialRefundsAmount(Collection $sessionIds, Fund $fund)
276
    {
277
        // Filter on debts that are not yet refunded.
278
        return PartialRefund::whereIn('session_id', $sessionIds)
279
            ->whereHas('debt', function($query) use($fund) {
280
                $query->interest()
281
                    ->whereDoesntHave('refund')
282
                    ->whereHas('loan', fn($query) => $query->where('fund_id', $fund->id));
283
            })
284
            ->sum('amount');
285
    }
286
287
    /**
288
     * @param Collection $sessionIds
289
     * @param Fund $fund
290
     *
291
     * @return int
292
     */
293
    private function getLoansAmount(Collection $sessionIds, Fund $fund)
294
    {
295
        return Debt::principal()
296
            ->whereHas('loan', fn($ql) => $ql->where('fund_id', $fund->id)
297
                ->whereIn('session_id', $sessionIds))
298
            ->sum('amount');
299
    }
300
301
    /**
302
     * @param Session $session    The session
303
     *
304
     * @return int
305
     */
306
    private function getFundsAmount(Session $session)
307
    {
308
        // Each fund can have a different set of sessions, so we need to loop on all funds.
309
        return $this->fundService->getSessionFunds($session)
310
            ->reduce(function(int $amount, Fund $fund) use($session) {
311
                $sessionIds = $this->fundService->getFundSessionIds($fund, $session);
312
313
                return $amount
314
                    + $this->getSavingsAmount($sessionIds, $fund)
315
                    + $this->getRefundsAmount($sessionIds, $fund)
316
                    + $this->getPartialRefundsAmount($sessionIds, $fund)
317
                    - $this->getLoansAmount($sessionIds, $fund);
318
            }, 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

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

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