Issues (213)

src/Service/Planning/BillSyncService.php (10 issues)

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

281
        $this->initBills(collect(/** @scrutinizer ignore-type */ [$charge]), $round->members);
Loading history...
282
        foreach($round->members as $member)
283
        {
284
            $this->createBills($charge, $member, $round, $round->sessions);
285
        }
286
        $this->saveBills();
287
    }
288
289
    /**
290
     * @param Round $round
291
     * @param Charge $charge
292
     *
293
     * @return void
294
     */
295
    public function chargeRemoved(Round $round, Charge $charge): void
0 ignored issues
show
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

295
    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...
296
    {
297
        $this->deleteBills($charge, 'charge_id');
298
    }
299
300
    /**
301
     * @param Round $round
302
     * @param Member $member
303
     *
304
     * @return void
305
     */
306
    public function memberEnabled(Round $round, Member $member): void
307
    {
308
        $charges = $round->charges->filter(fn($charge) =>
309
            $charge->def->is_fee && $charge->def->is_fixed);
310
        if($charges->count() === 0)
311
        {
312
            return;
313
        }
314
315
        $this->initBills($charges, collect([$member]));
0 ignored issues
show
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

315
        $this->initBills($charges, collect(/** @scrutinizer ignore-type */ [$member]));
Loading history...
316
        foreach($charges as $charge)
317
        {
318
            $this->createBills($charge, $member, $round, $round->sessions);
319
        }
320
        $this->saveBills();
321
    }
322
323
    /**
324
     * @param Round $round
325
     * @param Member $member
326
     *
327
     * @return void
328
     */
329
    public function memberRemoved(Round $round, Member $member): void
0 ignored issues
show
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

329
    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...
330
    {
331
        $this->deleteBills($member, 'member_id');
332
    }
333
334
    /**
335
     * @param Round $round
336
     * @param Collection|array $sessions
337
     *
338
     * @return void
339
     */
340
    public function sessionsCreated(Round $round, Collection|array $sessions): void
341
    {
342
        $charges = $round->charges->filter(fn($charge) =>
343
            $charge->def->is_fee && $charge->def->is_fixed);
344
        if($charges->count() === 0 || $round->members->count() === 0)
345
        {
346
            return;
347
        }
348
349
        $this->initBills($charges, $round->members);
350
        foreach($charges as $charge)
351
        {
352
            foreach($round->members as $member)
353
            {
354
                $this->createBills($charge, $member, $round, $sessions);
355
            }
356
        }
357
        $this->saveBills();
358
    }
359
360
    /**
361
     * @param Session $session
362
     *
363
     * @return void
364
     */
365
    public function sessionDeleted(Session $session)
366
    {
367
        $billIds = DB::table('session_bills')
368
            ->where('session_id', $session->id)
369
            ->select('bill_id')
370
            ->union(DB::table('libre_bills')
371
                ->where('session_id', $session->id)
372
                ->select('bill_id'))
373
            ->pluck('bill_id');
374
        // Will fail if a settlement exists for any of those bills.
375
        $session->session_bills()->delete();
376
        $session->libre_bills()->delete();
377
        DB::table('bills')->whereIn('id', $billIds)->delete();
378
    }
379
}
380