Passed
Pull Request — main (#48)
by Thierry
13:20
created

SessionService::getFineBills()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
nc 1
nop 2
dl 0
loc 12
rs 9.9666
c 1
b 0
f 0
1
<?php
2
3
namespace Siak\Tontine\Service\Report;
4
5
use Closure;
6
use Illuminate\Database\Query\Builder;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\DB;
9
use Siak\Tontine\Model\Debt;
10
use Siak\Tontine\Model\Member;
11
use Siak\Tontine\Model\Pool;
12
use Siak\Tontine\Model\Session;
13
use Siak\Tontine\Service\BalanceCalculator;
14
use Siak\Tontine\Service\TenantService;
15
16
use function collect;
17
use function count;
18
use function is_array;
19
20
class SessionService
21
{
22
    /**
23
     * @var TenantService
24
     */
25
    protected TenantService $tenantService;
26
27
    /**
28
     * @var BalanceCalculator
29
     */
30
    protected BalanceCalculator $balanceCalculator;
31
32
    /**
33
     * @param TenantService $tenantService
34
     * @param BalanceCalculator $balanceCalculator
35
     */
36
    public function __construct(TenantService $tenantService, BalanceCalculator $balanceCalculator)
37
    {
38
        $this->tenantService = $tenantService;
39
        $this->balanceCalculator = $balanceCalculator;
40
    }
41
42
    /**
43
     * @param Session $session
44
     *
45
     * @return Collection
46
     */
47
    public function getReceivables(Session $session): Collection
48
    {
49
        return Pool::where('round_id', $session->round_id)
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode...ion(...) { /* ... */ }) 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...
50
            ->withCount([
51
                'subscriptions as total_count' => function($query) use($session) {
52
                    $query->whereHas('receivables', function($query) use($session) {
53
                        $query->where('session_id', $session->id);
54
                    });
55
                },
56
                'subscriptions as paid_count' => function($query) use($session) {
57
                    $query->whereHas('receivables', function($query) use($session) {
58
                        $query->where('session_id', $session->id)->whereHas('deposit');
59
                    });
60
                },
61
            ])
62
            ->get()
63
            ->each(function($pool) use($session) {
64
                $pool->total_amount = $pool->amount * $pool->total_count;
65
                $pool->paid_amount = $this->balanceCalculator->getPoolDepositAmount($pool, $session);
66
            });
67
    }
68
69
    /**
70
     * @param Session $session
71
     *
72
     * @return Collection
73
     */
74
    public function getPayables(Session $session): Collection
75
    {
76
        return Pool::where('round_id', $session->round_id)
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode...ion(...) { /* ... */ }) 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...
77
            ->withCount([
78
                'subscriptions as total_count' => function($query) use($session) {
79
                    $query->whereHas('payable', function($query) use($session) {
80
                        $query->where('session_id', $session->id);
81
                    });
82
                },
83
                'subscriptions as paid_count' => function($query) use($session) {
84
                    $query->whereHas('payable', function($query) use($session) {
85
                        $query->where('session_id', $session->id)->whereHas('remitment');
86
                    });
87
                },
88
            ])
89
            ->get()
90
            ->each(function($pool) use($session) {
91
                $sessionCount = $this->tenantService->countEnabledSessions($pool);
92
                $pool->total_amount = $pool->amount * $pool->total_count * $sessionCount;
93
                $pool->paid_amount = $this->balanceCalculator->getPoolRemitmentAmount($pool, $session);
94
            });
95
    }
96
97
    /**
98
     * @param Session $session
99
     *
100
     * @return Collection
101
     */
102
    public function getAuctions(Session $session): Collection
103
    {
104
        return DB::table('auctions')
105
            ->select(DB::raw('sum(auctions.amount) as total'), 'subscriptions.pool_id')
106
            ->join('remitments', 'auctions.remitment_id', '=', 'remitments.id')
107
            ->join('payables', 'remitments.payable_id', '=', 'payables.id')
108
            ->join('subscriptions', 'payables.subscription_id', '=', 'subscriptions.id')
109
            ->where('auctions.session_id', $session->id)
110
            ->where('paid', true)
111
            ->groupBy('subscriptions.pool_id')
112
            ->pluck('total', 'pool_id');
113
    }
114
115
    /**
116
     * @param Closure $settlementFilter
117
     * @param Member $member|null
118
     *
119
     * @return Collection
120
     */
121
    private function getTontineBills(Closure $settlementFilter, ?Member $member = null): Collection
122
    {
123
        return DB::table('bills')
124
            ->join('tontine_bills', 'bills.id', '=', 'tontine_bills.bill_id')
125
            ->select(DB::raw('sum(bills.amount) as total_amount'),
126
                DB::raw('count(bills.id) as total_count'), 'tontine_bills.charge_id')
127
            ->groupBy('tontine_bills.charge_id')
128
            ->whereExists($settlementFilter)
129
            ->when($member !== null, function($query) use($member) {
130
                return $query->where('tontine_bills.member_id', $member->id);
131
            })
132
            ->get();
133
    }
134
135
    /**
136
     * @param Closure $settlementFilter
137
     * @param Member $member|null
138
     *
139
     * @return Collection
140
     */
141
    private function getRoundBills(Closure $settlementFilter, ?Member $member = null): Collection
142
    {
143
        return DB::table('bills')
144
            ->join('round_bills', 'bills.id', '=', 'round_bills.bill_id')
145
            ->select(DB::raw('sum(bills.amount) as total_amount'),
146
                DB::raw('count(bills.id) as total_count'), 'round_bills.charge_id')
147
            ->groupBy('round_bills.charge_id')
148
            ->whereExists($settlementFilter)
149
            ->when($member !== null, function($query) use($member) {
150
                return $query->where('round_bills.member_id', $member->id);
151
            })
152
            ->get();
153
    }
154
155
    /**
156
     * @param Closure $settlementFilter
157
     * @param Member $member|null
158
     *
159
     * @return Collection
160
     */
161
    private function getSessionBills(Closure $settlementFilter, ?Member $member = null): Collection
162
    {
163
        return DB::table('bills')
164
            ->join('session_bills', 'bills.id', '=', 'session_bills.bill_id')
165
            ->select(DB::raw('sum(bills.amount) as total_amount'),
166
                DB::raw('count(bills.id) as total_count'), 'session_bills.charge_id')
167
            ->groupBy('session_bills.charge_id')
168
            ->whereExists($settlementFilter)
169
            ->when($member !== null, function($query) use($member) {
170
                return $query->where('session_bills.member_id', $member->id);
171
            })
172
            ->get();
173
    }
174
175
    /**
176
     * @param Closure $settlementFilter
177
     * @param Member $member|null
178
     *
179
     * @return Collection
180
     */
181
    private function getFineBills(Closure $settlementFilter, ?Member $member = null): Collection
182
    {
183
        return DB::table('bills')
184
            ->join('fine_bills', 'bills.id', '=', 'fine_bills.bill_id')
185
            ->select(DB::raw('sum(bills.amount) as total_amount'),
186
                DB::raw('count(bills.id) as total_count'), 'fine_bills.charge_id')
187
            ->groupBy('fine_bills.charge_id')
188
            ->whereExists($settlementFilter)
189
            ->when($member !== null, function($query) use($member) {
190
                return $query->where('fine_bills.member_id', $member->id);
191
            })
192
            ->get();
193
    }
194
195
    /**
196
     * @param Collection $chargeIds
197
     * @param Collection $sessionIds
198
     *
199
     * @return Collection
200
     */
201
    public function getDisbursedAmounts(Collection $chargeIds, Collection $sessionIds): Collection
202
    {
203
        if($chargeIds->count() === 0)
204
        {
205
            return collect();
206
        }
207
208
        return DB::table('disbursements')
209
            ->select(DB::raw('sum(amount) as total_amount'),
210
                DB::raw('count(*) as total_count'), 'charge_id')
211
            ->groupBy('charge_id')
212
            ->whereIn('charge_id', $chargeIds)
213
            ->whereIn('session_id', $sessionIds)
214
            ->get()
215
            ->keyBy('charge_id');
216
    }
217
218
    /**
219
     * @param Session $session
220
     *
221
     * @return Collection
222
     */
223
    public function getSessionCharges(Session $session): Collection
224
    {
225
        $charges = $this->tenantService->tontine()->charges()->active()->get();
226
        $settlementFilter = function(Builder $query) use($session) {
227
            return $query->select(DB::raw(1))
228
                ->from('settlements')
229
                ->where('session_id', $session->id)
230
                ->whereColumn('settlements.bill_id', 'bills.id');
231
        };
232
        $bills = $this->getTontineBills($settlementFilter)
233
            ->concat($this->getRoundBills($settlementFilter))
234
            ->concat($this->getSessionBills($settlementFilter))
235
            ->concat($this->getFineBills($settlementFilter))
236
            ->keyBy('charge_id');
237
        $sessionIds = collect([$session->id]);
0 ignored issues
show
Bug introduced by
array($session->id) of type array<integer,integer> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

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

237
        $sessionIds = collect(/** @scrutinizer ignore-type */ [$session->id]);
Loading history...
238
        $disbursements = $this->getDisbursedAmounts($charges->pluck('id'), $sessionIds);
239
240
        return $charges->each(function($charge) use($bills, $disbursements) {
241
            $bill = $bills[$charge->id] ?? null;
242
            $charge->total_count = $bill ? $bill->total_count : 0;
243
            $charge->total_amount = $bill ? $bill->total_amount : 0;
244
            $charge->disbursement = $disbursements[$charge->id] ?? null;
245
        });
246
    }
247
248
    /**
249
     * @return int
250
     */
251
    private function countActiveMembers(): int
252
    {
253
        // The number of active members is saved in the round, so its current
254
        // value can be retrieved forever, even when the membership will change.
255
        $tontine = $this->tenantService->tontine();
256
        $round = $this->tenantService->round();
257
        if($round->property === null)
0 ignored issues
show
Bug introduced by
The property property does not seem to exist on Siak\Tontine\Model\Round. 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...
258
        {
259
            // Create and save the property with the content
260
            $memberIds = $tontine->members()->active()->pluck('id')->all();
261
            $round->property()->create(['content' => ['members' => $memberIds]]);
262
263
            return count($memberIds);
264
        }
265
266
        $content = $round->property->content;
267
        if(isset($content['members']) && is_array($content['members']))
268
        {
269
            // Return the existing content value
270
            return count($content['members']);
271
        }
272
273
        // Update the existing content with the members
274
        $content['members'] = $tontine->members()->active()->pluck('id')->all();
275
        $round->property->content = $content;
276
        $round->property->save();
277
278
        return count($content['members']);
279
    }
280
281
    /**
282
     * @param Session $session
283
     * @param Member $member|null
284
     *
285
     * @return Collection
286
     */
287
    public function getTotalCharges(Session $session, ?Member $member = null): Collection
288
    {
289
        $charges = $this->tenantService->tontine()->charges()->active()->get();
290
        $sessionIds = $this->tenantService->getSessionIds($session);
291
        $settlementFilter = function(Builder $query) use($sessionIds) {
292
            return $query->select(DB::raw(1))
293
                ->from('settlements')
294
                ->whereIn('session_id', $sessionIds)
295
                ->whereColumn('settlements.bill_id', 'bills.id');
296
        };
297
        $bills = $this->getTontineBills($settlementFilter, $member)
298
            ->concat($this->getRoundBills($settlementFilter, $member))
299
            ->concat($this->getSessionBills($settlementFilter, $member))
300
            ->concat($this->getFineBills($settlementFilter, $member))
301
            ->keyBy('charge_id');
302
        $disbursements = $this->getDisbursedAmounts($charges->pluck('id'), $sessionIds);
303
        if($member !== null)
304
        {
305
            // The disbursement part of each member id calculated by dividing each amount
306
            // by the number of members.
307
            $memberCount = $this->countActiveMembers();
308
            foreach($disbursements as $disbursement)
309
            {
310
                $disbursement->total_amount /= $memberCount;
311
            }
312
        }
313
314
        return $charges->each(function($charge) use($bills, $disbursements) {
315
            $bill = $bills[$charge->id] ?? null;
316
            $charge->total_count = $bill ? $bill->total_count : 0;
317
            $charge->total_amount = $bill ? $bill->total_amount : 0;
318
            $charge->disbursement = $disbursements[$charge->id] ?? null;
319
        });
320
    }
321
322
    /**
323
     * @param Session $session
324
     *
325
     * @return object
326
     */
327
    public function getLoan(Session $session): object
328
    {
329
        $principal = "CASE WHEN debts.type='" . Debt::TYPE_PRINCIPAL . "' THEN amount ELSE 0 END";
330
        $interest = "CASE WHEN debts.type='" . Debt::TYPE_INTEREST . "' THEN amount ELSE 0 END";
331
        $loan = DB::table('loans')
332
            ->join('debts', 'loans.id', '=', 'debts.loan_id')
333
            ->select(DB::raw("sum($principal) as principal"), DB::raw("sum($interest) as interest"))
334
            ->where('loans.session_id', $session->id)
335
            ->first();
336
        if(!$loan->principal)
337
        {
338
            $loan->principal = 0;
339
        }
340
        if(!$loan->interest)
341
        {
342
            $loan->interest = 0;
343
        }
344
345
        return $loan;
346
    }
347
348
    /**
349
     * @param Session $session
350
     *
351
     * @return object
352
     */
353
    public function getRefund(Session $session): object
354
    {
355
        $principal = "CASE WHEN debts.type='" . Debt::TYPE_PRINCIPAL . "' THEN amount ELSE 0 END";
356
        $interest = "CASE WHEN debts.type='" . Debt::TYPE_INTEREST . "' THEN amount ELSE 0 END";
357
        $refund = DB::table('refunds')
358
            ->join('debts', 'refunds.debt_id', '=', 'debts.id')
359
            ->select(DB::raw("sum($principal) as principal"), DB::raw("sum($interest) as interest"))
360
            ->where('refunds.session_id', $session->id)
361
            ->first();
362
        if(!$refund->principal)
363
        {
364
            $refund->principal = 0;
365
        }
366
        if(!$refund->interest)
367
        {
368
            $refund->interest = 0;
369
        }
370
371
        return $refund;
372
    }
373
374
    /**
375
     * @param Session $session
376
     *
377
     * @return object
378
     */
379
    public function getFunding(Session $session): object
380
    {
381
        $funding = DB::table('fundings')
382
            ->select(DB::raw('sum(amount) as total_amount'), DB::raw('count(id) as total_count'))
383
            ->where('session_id', $session->id)
384
            ->first();
385
        if(!$funding->total_amount)
386
        {
387
            $funding->total_amount = 0;
388
        }
389
        if(!$funding->total_count)
390
        {
391
            $funding->total_count = 0;
392
        }
393
394
        return $funding;
395
    }
396
397
    /**
398
     * @param Session $session
399
     *
400
     * @return object
401
     */
402
    public function getDisbursement(Session $session): object
403
    {
404
        $disbursement = DB::table('disbursements')
405
            ->select(DB::raw('sum(amount) as total_amount'), DB::raw('count(id) as total_count'))
406
            ->where('session_id', $session->id)
407
            ->first();
408
        if(!$disbursement->total_amount)
409
        {
410
            $disbursement->total_amount = 0;
411
        }
412
        if(!$disbursement->total_count)
413
        {
414
            $disbursement->total_count = 0;
415
        }
416
417
        return $disbursement;
418
    }
419
}
420