BillSyncService::createBill()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Siak\Tontine\Service\Planning;
4
5
use Illuminate\Support\Facades\DB;
6
use Illuminate\Support\Collection;
7
use Siak\Tontine\Model\Bill;
8
use Siak\Tontine\Model\Charge;
9
use Siak\Tontine\Model\Member;
10
use Siak\Tontine\Model\OnetimeBill;
11
use Siak\Tontine\Model\Round;
12
use Siak\Tontine\Model\RoundBill;
13
use Siak\Tontine\Model\Session;
14
use DateTime;
15
16
use function collect;
17
use function count;
18
use function now;
19
20
class BillSyncService
21
{
22
    /**
23
     * @var DateTime
24
     */
25
    private DateTime $today;
26
27
    /**
28
     * @var array
29
     */
30
    private array $existingBills;
31
32
    /**
33
     * @var array
34
     */
35
    private array $newBills;
36
37
    /**
38
     * @param Collection $charges
39
     * @param Collection $members
40
     *
41
     * @return void
42
     */
43
    private function initBills(Collection $charges, Collection $members): void
44
    {
45
        $this->today = now();
46
        $this->newBills = [
47
            'onetime_bills' => [],
48
            'round_bills' => [],
49
            'session_bills' => [],
50
        ];
51
        $this->existingBills = [
52
            'onetime_bills' => [],
53
            'round_bills' => [],
54
        ];
55
56
        // Avoid duplicates creation.
57
        $this->existingBills['onetime_bills'] = OnetimeBill::select([
58
                DB::raw('charges.def_id as charge_def_id'),
59
                DB::raw('members.def_id as member_def_id'),
60
            ])
61
            ->where(fn($qw) => $qw
62
                // Take the bills in the current round.
63
                ->orWhere(fn($qwb) => $qwb
64
                    ->whereIn('charge_id', $charges->pluck('id'))
65
                    ->whereIn('member_id', $members->pluck('id')))
66
                // Take the onetime bills already paid, including in other rounds.
67
                ->orWhere(fn($qwd) => $qwd
68
                    ->whereHas('charge', fn($qc) =>
69
                        $qc->once()
70
                            ->whereIn('def_id', $charges->pluck('def_id')))
71
                    ->whereHas('member', fn($qm) =>
72
                        $qm->whereIn('def_id', $members->pluck('def_id')))
73
                    ->whereHas('bill', fn($qb) => $qb->whereHas('settlement')))
74
            )
75
            ->join('charges', 'charges.id', '=', 'onetime_bills.charge_id')
76
            ->join('members', 'members.id', '=', 'onetime_bills.member_id')
77
            ->distinct()
78
            ->get();
79
        $this->existingBills['round_bills'] = RoundBill::select(['charge_id', 'member_id'])
80
            ->whereIn('charge_id', $charges->pluck('id'))
81
            ->whereIn('member_id', $members->pluck('id'))
82
            ->get();
83
    }
84
85
    /**
86
     * @return void
87
     */
88
    private function saveBills(): void
89
    {
90
        foreach(['onetime_bills', 'round_bills', 'session_bills'] as $table)
91
        {
92
            if(count($this->newBills[$table]) > 0)
93
            {
94
                DB::table($table)->insert($this->newBills[$table]);
95
            }
96
        }
97
    }
98
99
    /**
100
     * @param Charge $charge
101
     *
102
     * @return Bill
103
     */
104
    private function createBill(Charge $charge): Bill
105
    {
106
        return Bill::create([
0 ignored issues
show
Bug Best Practice introduced by
The expression return Siak\Tontine\Mode...d_at' => $this->today)) could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the type-hinted return Siak\Tontine\Model\Bill. Consider adding an additional type-check to rule them out.
Loading history...
107
            'charge' => $charge->def->name,
108
            'amount' => $charge->def->amount,
109
            'lendable' => $charge->def->lendable,
110
            'issued_at' => $this->today,
111
        ]);
112
    }
113
114
    /**
115
     * @param Charge $charge
116
     * @param Member $member
117
     *
118
     * @return void
119
     */
120
    private function createOnetimeBill(Charge $charge, Member $member): void
121
    {
122
        if($this->existingBills['onetime_bills']
123
            ->contains(fn(OnetimeBill $bill) =>
124
                $bill->charge_def_id === $charge->def_id &&
0 ignored issues
show
Bug introduced by
The property charge_def_id does not exist on Siak\Tontine\Model\OnetimeBill. Did you mean charge?
Loading history...
125
                $bill->member_def_id === $member->def_id))
0 ignored issues
show
Bug introduced by
The property member_def_id does not exist on Siak\Tontine\Model\OnetimeBill. Did you mean member?
Loading history...
126
        {
127
            return;
128
        }
129
130
        $bill = $this->createBill($charge);
131
        $this->newBills['onetime_bills'][] = [
132
            'bill_id' => $bill->id,
133
            'charge_id' => $charge->id,
134
            'member_id' => $member->id,
135
        ];
136
    }
137
138
    /**
139
     * @param Charge $charge
140
     * @param Member $member
141
     * @param Round $round
142
     *
143
     * @return void
144
     */
145
    private function createRoundBill(Charge $charge, Member $member, Round $round): void
146
    {
147
        if($this->existingBills['round_bills']
148
            ->contains(fn(RoundBill $bill) =>
149
                $bill->charge_id === $charge->id &&
150
                $bill->member_id === $member->id))
151
        {
152
            return;
153
        }
154
155
        $bill = $this->createBill($charge);
156
        $this->newBills['round_bills'][] = [
157
            'bill_id' => $bill->id,
158
            'charge_id' => $charge->id,
159
            'member_id' => $member->id,
160
            'round_id' => $round->id,
161
        ];
162
    }
163
164
    /**
165
     * @param Charge $charge
166
     * @param Member $member
167
     * @param Session $session
168
     *
169
     * @return void
170
     */
171
    private function createSessionBill(Charge $charge, Member $member, Session $session): void
172
    {
173
        $bill = $this->createBill($charge);
174
        $this->newBills['session_bills'][] = [
175
            'bill_id' => $bill->id,
176
            'charge_id' => $charge->id,
177
            'member_id' => $member->id,
178
            'session_id' => $session->id,
179
        ];
180
    }
181
182
    /**
183
     * @param Charge $charge
184
     * @param Member $member
185
     * @param Round $round
186
     * @param Collection|array $sessions
187
     *
188
     * @return void
189
     */
190
    private function createBills(Charge $charge, Member $member,
191
        Round $round, Collection|array $sessions): void
192
    {
193
        if($charge->def->period_once)
0 ignored issues
show
Bug introduced by
The property period_once does not exist on Siak\Tontine\Model\ChargeDef. Did you mean period?
Loading history...
194
        {
195
            $this->createOnetimeBill($charge, $member);
196
            return;
197
        }
198
        if($charge->def->period_round)
0 ignored issues
show
Bug introduced by
The property period_round does not exist on Siak\Tontine\Model\ChargeDef. Did you mean period?
Loading history...
199
        {
200
            $this->createRoundBill($charge, $member, $round);
201
            return;
202
        }
203
        foreach($sessions as $session)
204
        {
205
            $this->createSessionBill($charge, $member, $session);
206
        }
207
    }
208
209
    /**
210
     * @param Charge|Member $owner
211
     * @param string $foreignKey
212
     *
213
     * @return void
214
     */
215
    private function deleteBills(Charge|Member $owner, string $foreignKey): void
216
    {
217
        $billIds = DB::table('onetime_bills')
218
            ->where($foreignKey, $owner->id)
219
            ->select('bill_id')
220
            ->union(DB::table('round_bills')
221
                ->where($foreignKey, $owner->id)
222
                ->select('bill_id'))
223
            ->union(DB::table('session_bills')
224
                ->where($foreignKey, $owner->id)
225
                ->select('bill_id'))
226
            ->union(DB::table('libre_bills')
227
                ->where($foreignKey, $owner->id)
228
                ->select('bill_id'))
229
            ->pluck('bill_id');
230
        if($billIds->count() === 0)
231
        {
232
            return;
233
        }
234
235
        // Will fail if a settlement exists for any of those bills.
236
        $owner->onetime_bills()->delete();
237
        $owner->round_bills()->delete();
238
        $owner->session_bills()->delete();
239
        $owner->libre_bills()->delete();
240
        DB::table('bills')->whereIn('id', $billIds)->delete();
241
    }
242
243
    /**
244
     * @param Round $round
245
     * @param Charge $charge
246
     *
247
     * @return void
248
     */
249
    public function chargeEnabled(Round $round, Charge $charge): void
250
    {
251
        if($charge->def->is_fine || $charge->def->is_variable || $round->members->count() === 0)
0 ignored issues
show
Bug introduced by
The property is_fine does not seem to exist on Siak\Tontine\Model\ChargeDef. 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 is_variable does not seem to exist on Siak\Tontine\Model\ChargeDef. 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...
252
        {
253
            return;
254
        }
255
256
        $this->initBills(collect([$charge]), $round->members);
0 ignored issues
show
Bug introduced by
array($charge) of type array<integer,Siak\Tontine\Model\Charge> 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

256
        $this->initBills(collect(/** @scrutinizer ignore-type */ [$charge]), $round->members);
Loading history...
257
        foreach($round->members as $member)
258
        {
259
            $this->createBills($charge, $member, $round, $round->sessions);
260
        }
261
        $this->saveBills();
262
    }
263
264
    /**
265
     * @param Round $round
266
     * @param Charge $charge
267
     *
268
     * @return void
269
     */
270
    public function chargeRemoved(Round $round, Charge $charge): void
0 ignored issues
show
Unused Code introduced by
The parameter $round is not used and could be removed. ( Ignorable by Annotation )

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

270
    public function chargeRemoved(/** @scrutinizer ignore-unused */ Round $round, Charge $charge): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
271
    {
272
        $this->deleteBills($charge, 'charge_id');
273
    }
274
275
    /**
276
     * @param Round $round
277
     * @param Member $member
278
     *
279
     * @return void
280
     */
281
    public function memberEnabled(Round $round, Member $member): void
282
    {
283
        $charges = $round->charges->filter(fn($charge) =>
284
            $charge->def->is_fee && $charge->def->is_fixed);
285
        if($charges->count() === 0)
286
        {
287
            return;
288
        }
289
290
        $this->initBills($charges, collect([$member]));
0 ignored issues
show
Bug introduced by
array($member) of type array<integer,Siak\Tontine\Model\Member> 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

290
        $this->initBills($charges, collect(/** @scrutinizer ignore-type */ [$member]));
Loading history...
291
        foreach($charges as $charge)
292
        {
293
            $this->createBills($charge, $member, $round, $round->sessions);
294
        }
295
        $this->saveBills();
296
    }
297
298
    /**
299
     * @param Round $round
300
     * @param Member $member
301
     *
302
     * @return void
303
     */
304
    public function memberRemoved(Round $round, Member $member): void
0 ignored issues
show
Unused Code introduced by
The parameter $round is not used and could be removed. ( Ignorable by Annotation )

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

304
    public function memberRemoved(/** @scrutinizer ignore-unused */ Round $round, Member $member): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
305
    {
306
        $this->deleteBills($member, 'member_id');
307
    }
308
309
    /**
310
     * @param Round $round
311
     * @param Collection|array $sessions
312
     *
313
     * @return void
314
     */
315
    public function sessionsCreated(Round $round, Collection|array $sessions): void
316
    {
317
        $charges = $round->charges->filter(fn($charge) =>
318
            $charge->def->is_fee && $charge->def->is_fixed);
319
        if($charges->count() === 0 || $round->members->count() === 0)
320
        {
321
            return;
322
        }
323
324
        $this->initBills($charges, $round->members);
325
        foreach($charges as $charge)
326
        {
327
            foreach($round->members as $member)
328
            {
329
                $this->createBills($charge, $member, $round, $sessions);
330
            }
331
        }
332
        $this->saveBills();
333
    }
334
335
    /**
336
     * @param Session $session
337
     *
338
     * @return void
339
     */
340
    public function sessionDeleted(Session $session)
341
    {
342
        $billIds = DB::table('session_bills')
343
            ->where('session_id', $session->id)
344
            ->select('bill_id')
345
            ->union(DB::table('libre_bills')
346
                ->where('session_id', $session->id)
347
                ->select('bill_id'))
348
            ->pluck('bill_id');
349
        // Will fail if a settlement exists for any of those bills.
350
        $session->session_bills()->delete();
351
        $session->libre_bills()->delete();
352
        DB::table('bills')->whereIn('id', $billIds)->delete();
353
    }
354
}
355