Passed
Push — main ( 0b6b4a...973494 )
by Thierry
06:50
created

BillService::getSettlementAmount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
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
            ->with('session')
49
            ->where('charge_id', $charge->id)
50
            ->search($this->searchSanitizer->sanitize($search))
51
            ->when($onlyPaid === false, fn($query) => $query->unpaid())
52
            ->when($onlyPaid === true, function($query) use($session) {
53
                $query->whereHas('settlement',
54
                    fn(Builder $qs) => $qs->where('session_id', $session->id));
55
            });
56
    }
57
58
    /**
59
     * @param Charge $charge
60
     * @param Session $session
61
     * @param string $search
62
     * @param bool $onlyPaid|null
63
     *
64
     * @return int
65
     */
66
    public function getBillCount(Charge $charge, Session $session,
67
        string $search = '', ?bool $onlyPaid = null): int
68
    {
69
        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...
70
    }
71
72
    /**
73
     * @param Charge $charge
74
     * @param Session $session
75
     * @param string $search
76
     * @param bool $onlyPaid|null
77
     * @param int $page
78
     *
79
     * @return Collection
80
     */
81
    public function getBills(Charge $charge, Session $session,
82
        string $search = '', ?bool $onlyPaid = null, int $page = 0): Collection
83
    {
84
        return $this->getBillsQuery($charge, $session, $search, $onlyPaid)
85
            ->with('settlement')
86
            ->page($page, $this->tenantService->getLimit())
87
            ->orderBy('member', 'asc')
88
            ->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

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