BillService   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 132
dl 0
loc 408
rs 9.84
c 5
b 0
f 0
wmc 32

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getBills() 0 9 1
A getBillCount() 0 4 1
A __construct() 0 4 1
A getMember() 0 26 4
A updateBill() 0 10 2
A deleteBill() 0 10 2
A createBill() 0 15 3
A _deleteBills() 0 17 2
A getMembersQuery() 0 12 1
A getMembers() 0 27 3
A getMemberCount() 0 4 1
A getMemberBillQuery() 0 7 1
A deleteBills() 0 10 1
A getBillTotal() 0 14 1
A getMemberBill() 0 3 1
A _createBill() 0 21 3
A createBills() 0 10 2
A getBillsQuery() 0 10 1
A getBill() 0 4 1
1
<?php
2
3
namespace Siak\Tontine\Service\Meeting\Charge;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Relations\Relation;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\DB;
9
use Siak\Tontine\Exception\MessageException;
10
use Siak\Tontine\Model\Bill;
11
use Siak\Tontine\Model\Charge;
12
use Siak\Tontine\Model\LibreBill;
13
use Siak\Tontine\Model\Member;
14
use Siak\Tontine\Model\Session;
15
use Siak\Tontine\Model\Settlement;
16
use Siak\Tontine\Service\LocaleService;
17
use Siak\Tontine\Service\TenantService;
18
use Siak\Tontine\Validation\SearchSanitizer;
19
use Exception;
20
21
use function trans;
22
23
class BillService
24
{
25
    /**
26
     * @param SettlementTargetService $targetService
27
     * @param TenantService $tenantService
28
     * @param LocaleService $localeService
29
     * @param SearchSanitizer $searchSanitizer
30
     */
31
    public function __construct(protected SettlementTargetService $targetService,
32
        protected TenantService $tenantService, protected LocaleService $localeService,
33
        private SearchSanitizer $searchSanitizer)
34
    {}
35
36
    /**
37
     * @param Charge $charge
38
     * @param Session $session
39
     * @param string $search
40
     * @param bool $onlyPaid|null
41
     *
42
     * @return Builder
43
     */
44
    private function getBillsQuery(Charge $charge, Session $session,
45
        string $search = '', ?bool $onlyPaid = null): Builder|Relation
46
    {
47
        return Bill::forSession($session)
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode...ion(...) { /* ... */ }) could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the type-hinted return Illuminate\Database\Eloq...uent\Relations\Relation. Consider adding an additional type-check to rule them out.
Loading history...
48
            ->whereCharge($charge)
49
            ->search($this->searchSanitizer->sanitize($search))
50
            ->when($onlyPaid === false, fn($query) => $query->unpaid())
51
            ->when($onlyPaid === true, function($query) use($session) {
52
                $query->whereHas('settlement',
53
                    fn(Builder $qs) => $qs->where('session_id', $session->id));
54
            });
55
    }
56
57
    /**
58
     * @param Charge $charge
59
     * @param Session $session
60
     * @param string $search
61
     * @param bool $onlyPaid|null
62
     *
63
     * @return int
64
     */
65
    public function getBillCount(Charge $charge, Session $session,
66
        string $search = '', ?bool $onlyPaid = null): int
67
    {
68
        return $this->getBillsQuery($charge, $session, $search, $onlyPaid)->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getBillsQu...ch, $onlyPaid)->count() could return the type Illuminate\Database\Eloq...uent\Relations\Relation which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
69
    }
70
71
    /**
72
     * @param Charge $charge
73
     * @param Session $session
74
     * @param string $search
75
     * @param bool $onlyPaid|null
76
     * @param int $page
77
     *
78
     * @return Collection
79
     */
80
    public function getBills(Charge $charge, Session $session,
81
        string $search = '', ?bool $onlyPaid = null, int $page = 0): Collection
82
    {
83
        return $this->getBillsQuery($charge, $session, $search, $onlyPaid)
84
            ->with(['session', 'settlement', 'settlement.fund'])
85
            ->page($page, $this->tenantService->getLimit())
86
            ->orderBy('member', 'asc')
87
            ->orderBy('bill_date', 'asc')
0 ignored issues
show
Bug introduced by
'bill_date' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

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

87
            ->orderBy(/** @scrutinizer ignore-type */ 'bill_date', 'asc')
Loading history...
88
            ->get();
89
    }
90
91
    /**
92
     * @param Charge $charge
93
     * @param Session $session
94
     * @param int $billId
95
     *
96
     * @return Bill|null
97
     */
98
    public function getBill(Charge $charge, Session $session, int $billId): ?Bill
99
    {
100
        return $this->getBillsQuery($charge, $session)
101
            ->find($billId);
102
    }
103
104
    /**
105
     * @param Charge $charge
106
     * @param Session $session
107
     * @param bool $onlyPaid|null
108
     *
109
     * @return array<int>
110
     */
111
    public function getBillTotal(Charge $charge, Session $session, ?bool $onlyPaid = null): array
112
    {
113
        $total = LibreBill::query()
114
            ->join('bills', 'bills.id', '=', 'libre_bills.bill_id')
115
            ->whereCharge($charge, true)->whereSession($session)
116
            ->when($onlyPaid === false, fn($query) => $query->whereHas('bill',
117
                fn(Builder $qb) => $qb->whereDoesntHave('settlement',
118
                    fn(Builder $qs) => $qs->whereSession($session))))
119
            ->when($onlyPaid === true, fn($query) => $query->whereHas('bill',
120
                fn(Builder $qb) => $qb->whereHas('settlement',
121
                    fn(Builder $qs) => $qs->whereSession($session))))
122
            ->select(DB::raw('count(*) as count'), DB::raw('sum(bills.amount) as amount'))
123
            ->first();
124
        return [$total->count ?? 0, $total->amount ?? 0];
125
    }
126
127
    /**
128
     * @param Charge $charge
129
     * @param Session $session
130
     * @param string $search
131
     * @param bool $filter|null
132
     *
133
     * @return Builder|Relation
134
     */
135
    private function getMembersQuery(Charge $charge, Session $session,
136
        string $search = '', ?bool $filter = null): Builder|Relation
137
    {
138
        $filterFunction = fn($query) => $query
139
            ->where('charge_id', $charge->id)->where('session_id', $session->id);
140
141
        return $session->members()
142
            ->search($this->searchSanitizer->sanitize($search))
143
            ->when($filter === false, fn($query) => $query
144
                ->whereDoesntHave('libre_bills', $filterFunction))
145
            ->when($filter === true, fn($query) => $query
146
                ->whereHas('libre_bills', $filterFunction));
147
    }
148
149
    /**
150
     * @param Charge $charge
151
     * @param Session $session
152
     * @param string $search
153
     * @param bool $filter|null
154
     * @param int $page
155
     *
156
     * @return Collection
157
     */
158
    public function getMembers(Charge $charge, Session $session,
159
        string $search = '', ?bool $filter = null, int $page = 0): Collection
160
    {
161
        $members = $this->getMembersQuery($charge, $session, $search, $filter)
162
            ->page($page, $this->tenantService->getLimit())
163
            ->with([
164
                'libre_bills' => fn($query) => $query
165
                    ->where('charge_id', $charge->id)->where('session_id', $session->id),
166
            ])
167
            ->orderBy('name', 'asc')
168
            ->get()
169
            ->each(function($member) {
170
                $member->remaining = 0;
171
                $member->bill = $member->libre_bills->first()?->bill ?? null;
172
            });
173
        // Check if there is a settlement target.
174
        if(!($target = $this->targetService->getTarget($charge, $session)))
175
        {
176
            return $members;
177
        }
178
179
        $settlements = $this->targetService->getMembersSettlements($members, $charge, $target);
180
181
        return $members->each(function($member) use($target, $settlements) {
182
            $member->target = $target->amount;
183
            $paid = $settlements[$member->id] ?? 0;
184
            $member->remaining = $target->amount > $paid ? $target->amount - $paid : 0;
185
        });
186
    }
187
188
    /**
189
     * @param Charge $charge
190
     * @param Session $session
191
     * @param string $search
192
     * @param bool $filter|null
193
     *
194
     * @return int
195
     */
196
    public function getMemberCount(Charge $charge, Session $session,
197
        string $search = '', ?bool $filter = null): int
198
    {
199
        return $this->getMembersQuery($charge, $session, $search, $filter)->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMembers...arch, $filter)->count() could return the type Illuminate\Database\Eloq...uent\Relations\Relation which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
200
    }
201
202
    /**
203
     * @param Charge $charge
204
     * @param Session $session
205
     * @param int $memberId
206
     *
207
     * @return Member|null
208
     */
209
    public function getMember(Charge $charge, Session $session, int $memberId): ?Member
210
    {
211
        $members = $this->getMembersQuery($charge, $session)
212
            ->where('members.id', $memberId)
213
            ->with([
214
                'libre_bills.bill',
215
                'libre_bills' => fn($query) => $query
216
                    ->where('charge_id', $charge->id)->where('session_id', $session->id),
217
            ])
218
            ->get();
219
        if($members->count() === 0)
220
        {
221
            return null;
222
        }
223
224
        $member = $members->first();
225
        $member->bill = $member->libre_bills->first()?->bill ?? null;
226
        $member->remaining = 0;
227
        // Check if there is a settlement target.
228
        if(($target = $this->targetService->getTarget($charge, $session)) !== null)
229
        {
230
            $settlements = $this->targetService->getMembersSettlements($members, $charge, $target);
231
            $paid = $settlements[$member->id] ?? 0;
232
            $member->remaining = $target->amount > $paid ? $target->amount - $paid : 0;
233
        }
234
        return $member;
235
    }
236
237
    /**
238
     * @param Charge $charge
239
     * @param Session $session
240
     * @param Member $member
241
     * @param bool $paid
242
     * @param float $amount
243
     *
244
     * @return void
245
     */
246
    private function _createBill(Charge $charge, Session $session, Member $member,
247
        bool $paid, float $amount): void
248
    {
249
        $bill = Bill::create([
250
            'charge' => $charge->name,
251
            'amount' => $charge->has_amount ? $charge->amount : $amount,
0 ignored issues
show
Bug introduced by
The property has_amount does not exist on Siak\Tontine\Model\Charge. Did you mean amount?
Loading history...
252
            'lendable' => $charge->lendable,
253
            'issued_at' => now(),
254
        ]);
255
        $libreBill = new LibreBill();
256
        $libreBill->charge()->associate($charge);
257
        $libreBill->member()->associate($member);
258
        $libreBill->session()->associate($session);
259
        $libreBill->bill()->associate($bill);
260
        $libreBill->save();
261
        if($paid)
262
        {
263
            $settlement = new Settlement();
264
            $settlement->bill()->associate($bill);
265
            $settlement->session()->associate($session);
266
            $settlement->save();
267
        }
268
    }
269
270
    /**
271
     * @param Charge $charge
272
     * @param Session $session
273
     * @param int $memberId
274
     * @param bool $paid
275
     * @param float $amount
276
     *
277
     * @return void
278
     */
279
    public function createBill(Charge $charge, Session $session, int $memberId,
280
        bool $paid, float $amount = 0): void
281
    {
282
        $member = $session->members()->find($memberId);
283
        if(!$member)
284
        {
285
            throw new MessageException(trans('tontine.member.errors.not_found'));
286
        }
287
288
        if($amount !== 0)
289
        {
290
            $amount = $this->localeService->convertMoneyToInt($amount);
291
        }
292
        DB::transaction(function() use($charge, $session, $member, $paid, $amount) {
293
            $this->_createBill($charge, $session, $member, $paid, $amount);
294
        });
295
    }
296
297
    /**
298
     * @param Charge $charge
299
     * @param Session $session
300
     * @param string $search
301
     * @param bool $paid
302
     * @param float $amount
303
     *
304
     * @return void
305
     */
306
    public function createBills(Charge $charge, Session $session, string $search,
307
        bool $paid, float $amount): void
308
    {
309
        $members = $this->getMembersQuery($charge, $session, $search,
310
            false)->get();
311
        DB::transaction(function() use($charge, $session, $members, $paid, $amount) {
312
            // Todo: use one insert query
313
            foreach($members as $member)
314
            {
315
                $this->_createBill($charge, $session, $member, $paid, $amount);
316
            }
317
        });
318
    }
319
320
    /**
321
     * @param Session $session
322
     * @param Collection $billIds
323
     *
324
     * @return void
325
     */
326
    public function _deleteBills(Session $session, Collection $billIds): void
327
    {
328
        try
329
        {
330
            DB::transaction(function() use($session, $billIds) {
331
                LibreBill::query()->whereIn('bill_id', $billIds)->delete();
332
                // Delete a settlement only if it is on the same session
333
                Settlement::query()
334
                    ->whereSession($session)
335
                    ->whereIn('bill_id', $billIds)
336
                    ->delete();
337
                Bill::query()->whereIn('id', $billIds)->delete();
338
            });
339
        }
340
        catch(Exception $ex)
341
        {
342
            throw new MessageException(trans('meeting.bill.errors.delete'));
343
        }
344
    }
345
346
    /**
347
     * @param Charge $charge
348
     * @param Session $session
349
     * @param string $search
350
     *
351
     * @return void
352
     */
353
    public function deleteBills(Charge $charge, Session $session, string $search): void
354
    {
355
        $members = $this->getMembersQuery($charge, $session, $search)
356
            ->pluck('id');
357
        $billIds = LibreBill::query()
358
            ->whereSession($session)
359
            ->whereCharge($charge)
360
            ->whereMembers($members)
361
            ->pluck('bill_id');
362
        $this->_deleteBills($session, $billIds);
363
    }
364
365
    /**
366
     * @param Charge $charge
367
     * @param Session $session
368
     * @param int $memberId
369
     *
370
     * @return Builder|Relation
371
     */
372
    private function getMemberBillQuery(Charge $charge, Session $session,
373
        int $memberId): Builder|Relation
374
    {
375
        return LibreBill::query()
376
            ->whereSession($session)
377
            ->whereCharge($charge)
378
            ->where('member_id', $memberId);
379
    }
380
381
    /**
382
     * @param Charge $charge
383
     * @param Session $session
384
     * @param int $memberId
385
     *
386
     * @return LibreBill|null
387
     */
388
    public function getMemberBill(Charge $charge, Session $session, int $memberId): ?LibreBill
389
    {
390
        return $this->getMemberBillQuery($charge, $session, $memberId)->first();
391
    }
392
393
    /**
394
     * @param Charge $charge
395
     * @param Session $session
396
     * @param int $memberId
397
     * @param float $amount
398
     *
399
     * @return void
400
     */
401
    public function updateBill(Charge $charge, Session $session, int $memberId, float $amount): void
402
    {
403
        $libreBill = $this->getMemberBill($charge, $session, $memberId);
404
        if(!$libreBill)
405
        {
406
            return; // throw new MessageException(trans('tontine.bill.errors.not_found'));
407
        }
408
409
        $libreBill->bill->update([
410
            'amount'=> $this->localeService->convertMoneyToInt($amount),
411
        ]);
412
    }
413
414
    /**
415
     * @param Charge $charge
416
     * @param Session $session
417
     * @param int $memberId
418
     *
419
     * @return void
420
     */
421
    public function deleteBill(Charge $charge, Session $session, int $memberId): void
422
    {
423
        $billIds = $this->getMemberBillQuery($charge, $session, $memberId)
424
            ->pluck('bill_id');
425
        if($billIds->count() === 0)
426
        {
427
            return; // throw new MessageException(trans('tontine.bill.errors.not_found'));
428
        }
429
430
        $this->_deleteBills($session, $billIds);    }
431
}
432