BillService::createBill()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 22
c 0
b 0
f 0
nc 3
nop 6
dl 0
loc 32
rs 9.2568
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\Round;
15
use Siak\Tontine\Model\Session;
16
use Siak\Tontine\Model\Settlement;
17
use Siak\Tontine\Service\LocaleService;
18
use Siak\Tontine\Service\TenantService;
19
use Siak\Tontine\Validation\SearchSanitizer;
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::ofSession($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 Bill
109
     */
110
    public function getSettlementCount(Charge $charge, Session $session): Bill
111
    {
112
        return $this->getBillsQuery($charge, $session, '', true)
113
            ->select(DB::raw('count(*) as total'), DB::raw('sum(bills.amount) as amount'))
114
            ->first();
115
    }
116
117
    /**
118
     * @param Round $round
119
     * @param Charge $charge
120
     * @param Session $session
121
     * @param string $search
122
     * @param bool $filter|null
123
     *
124
     * @return Builder|Relation
125
     */
126
    private function getMembersQuery(Round $round, Charge $charge, Session $session,
127
        string $search = '', ?bool $filter = null): Builder|Relation
128
    {
129
        $filterFunction = fn($query) => $query
130
            ->where('charge_id', $charge->id)->where('session_id', $session->id);
131
132
        return $round->members()
133
            ->search($this->searchSanitizer->sanitize($search))
134
            ->when($filter === false, fn($query) => $query
135
                ->whereDoesntHave('libre_bills', $filterFunction))
136
            ->when($filter === true, fn($query) => $query
137
                ->whereHas('libre_bills', $filterFunction));
138
    }
139
140
    /**
141
     * @param Round $round
142
     * @param Charge $charge
143
     * @param Session $session
144
     * @param string $search
145
     * @param bool $filter|null
146
     * @param int $page
147
     *
148
     * @return Collection
149
     */
150
    public function getMembers(Round $round, Charge $charge, Session $session,
151
        string $search = '', ?bool $filter = null, int $page = 0): Collection
152
    {
153
        $members = $this->getMembersQuery($round, $charge, $session, $search, $filter)
154
            ->page($page, $this->tenantService->getLimit())
155
            ->with([
156
                'libre_bills' => fn($query) => $query
157
                    ->where('charge_id', $charge->id)->where('session_id', $session->id),
158
            ])
159
            ->orderBy('name', 'asc')
160
            ->get()
161
            ->each(function($member) {
162
                $member->remaining = 0;
163
                $member->bill = $member->libre_bills->first()?->bill ?? null;
164
            });
165
        // Check if there is a settlement target.
166
        if(!($target = $this->targetService->getTarget($charge, $session)))
167
        {
168
            return $members;
169
        }
170
171
        $settlements = $this->targetService->getMembersSettlements($members, $charge, $target);
172
173
        return $members->each(function($member) use($target, $settlements) {
174
            $member->target = $target->amount;
175
            $paid = $settlements[$member->id] ?? 0;
176
            $member->remaining = $target->amount > $paid ? $target->amount - $paid : 0;
177
        });
178
    }
179
180
    /**
181
     * @param Round $round
182
     * @param Charge $charge
183
     * @param Session $session
184
     * @param string $search
185
     * @param bool $filter|null
186
     *
187
     * @return int
188
     */
189
    public function getMemberCount(Round $round, Charge $charge, Session $session,
190
        string $search = '', ?bool $filter = null): int
191
    {
192
        return $this->getMembersQuery($round, $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...
193
    }
194
195
    /**
196
     * @param Round $round
197
     * @param Charge $charge
198
     * @param Session $session
199
     * @param int $memberId
200
     *
201
     * @return Member|null
202
     */
203
    public function getMember(Round $round, Charge $charge, Session $session, int $memberId): ?Member
204
    {
205
        $members = $this->getMembersQuery($round, $charge, $session)
206
            ->where('members.id', $memberId)
207
            ->with([
208
                'libre_bills.bill',
209
                'libre_bills' => fn($query) => $query
210
                    ->where('charge_id', $charge->id)->where('session_id', $session->id),
211
            ])
212
            ->get();
213
        if($members->count() === 0)
214
        {
215
            return null;
216
        }
217
218
        $member = $members->first();
219
        $member->bill = $member->libre_bills->first()?->bill ?? null;
220
        $member->remaining = 0;
221
        // Check if there is a settlement target.
222
        if(($target = $this->targetService->getTarget($charge, $session)) !== null)
223
        {
224
            $settlements = $this->targetService->getMembersSettlements($members, $charge, $target);
225
            $paid = $settlements[$member->id] ?? 0;
226
            $member->remaining = $target->amount > $paid ? $target->amount - $paid : 0;
227
        }
228
        return $member;
229
    }
230
231
    /**
232
     * @param Round $round
233
     * @param Charge $charge
234
     * @param Session $session
235
     * @param int $memberId
236
     * @param bool $paid
237
     * @param float $amount
238
     *
239
     * @return void
240
     */
241
    public function createBill(Round $round, Charge $charge, Session $session,
242
        int $memberId, bool $paid, float $amount = 0): void
243
    {
244
        $member = $round->members()->find($memberId);
245
        if(!$member)
246
        {
247
            throw new MessageException(trans('tontine.member.errors.not_found'));
248
        }
249
250
        if($amount !== 0)
251
        {
252
            $amount = $this->localeService->convertMoneyToInt($amount);
253
        }
254
        DB::transaction(function() use($charge, $session, $member, $paid, $amount) {
255
            $bill = Bill::create([
256
                'charge' => $charge->name,
257
                '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...
258
                'lendable' => $charge->lendable,
259
                'issued_at' => now(),
260
            ]);
261
            $libreBill = new LibreBill();
262
            $libreBill->charge()->associate($charge);
263
            $libreBill->member()->associate($member);
264
            $libreBill->session()->associate($session);
265
            $libreBill->bill()->associate($bill);
266
            $libreBill->save();
267
            if($paid)
268
            {
269
                $settlement = new Settlement();
270
                $settlement->bill()->associate($bill);
271
                $settlement->session()->associate($session);
272
                $settlement->save();
273
            }
274
        });
275
    }
276
277
    /**
278
     * @param Round $round
279
     * @param Charge $charge
280
     * @param Session $session
281
     * @param int $memberId
282
     *
283
     * @return LibreBill|null
284
     */
285
    public function getMemberBill(Round $round, Charge $charge, Session $session, int $memberId): ?LibreBill
286
    {
287
        if(!($member = $round->members()->find($memberId)))
288
        {
289
            throw new MessageException(trans('tontine.member.errors.not_found'));
290
        }
291
292
        return LibreBill::with(['bill.settlement'])
293
            ->where('charge_id', $charge->id)
294
            ->where('session_id', $session->id)
295
            ->where('member_id', $member->id)
296
            ->first();
297
    }
298
299
    /**
300
     * @param Round $round
301
     * @param Charge $charge
302
     * @param Session $session
303
     * @param int $memberId
304
     * @param float $amount
305
     *
306
     * @return void
307
     */
308
    public function updateBill(Round $round, Charge $charge, Session $session, int $memberId, float $amount): void
309
    {
310
        if(!($libreBill = $this->getMemberBill($round, $charge, $session, $memberId)))
311
        {
312
            return; // throw new MessageException(trans('tontine.bill.errors.not_found'));
313
        }
314
315
        $libreBill->bill->update([
316
            'amount'=> $this->localeService->convertMoneyToInt($amount),
317
        ]);
318
    }
319
320
    /**
321
     * @param Round $round
322
     * @param Charge $charge
323
     * @param Session $session
324
     * @param int $memberId
325
     *
326
     * @return void
327
     */
328
    public function deleteBill(Round $round, Charge $charge, Session $session, int $memberId): void
329
    {
330
        if(!($libreBill = $this->getMemberBill($round, $charge, $session, $memberId)))
331
        {
332
            return; // throw new MessageException(trans('tontine.bill.errors.not_found'));
333
        }
334
335
        DB::transaction(function() use($libreBill, $session) {
336
            $bill = $libreBill->bill;
337
            $libreBill->delete();
338
            if($bill !== null)
339
            {
340
                // Delete the settlement only if it is on the same session
341
                if($bill->settlement !== null && $bill->settlement->session_id === $session->id)
342
                {
343
                    $bill->settlement->delete();
344
                }
345
                $bill->delete();
346
            }
347
        });
348
    }
349
}
350