RemitmentService::getSubscriptions()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 4
eloc 17
c 3
b 0
f 0
nc 2
nop 2
dl 0
loc 23
rs 9.7
1
<?php
2
3
namespace Siak\Tontine\Service\Meeting\Pool;
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\Auction;
11
use Siak\Tontine\Model\Pool;
12
use Siak\Tontine\Model\Payable;
13
use Siak\Tontine\Model\Session;
14
use Siak\Tontine\Service\LocaleService;
15
use Siak\Tontine\Service\Meeting\Session\SummaryService;
16
use Siak\Tontine\Service\Payment\BalanceCalculator;
17
use Siak\Tontine\Service\TenantService;
18
use Siak\Tontine\Service\Planning\PoolService;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Siak\Tontine\Service\Meeting\Pool\PoolService. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
20
use function trans;
21
22
class RemitmentService
23
{
24
    /**
25
     * @param BalanceCalculator $balanceCalculator
26
     * @param LocaleService $localeService
27
     * @param TenantService $tenantService
28
     * @param PoolService $poolService
29
     * @param SummaryService $summaryService
30
     */
31
    public function __construct(private BalanceCalculator $balanceCalculator,
32
        private LocaleService $localeService, private TenantService $tenantService,
33
        private PoolService $poolService, private SummaryService $summaryService)
34
    {}
35
36
    /**
37
     * @param Pool $pool
38
     * @param Session $session
39
     *
40
     * @return Builder|Relation
41
     */
42
    private function getQuery(Pool $pool, Session $session): Builder|Relation
43
    {
44
        return $session->payables()
45
            ->join('subscriptions', 'subscriptions.id', '=', 'payables.subscription_id')
46
            ->where('subscriptions.pool_id', $pool->id);
47
    }
48
49
    /**
50
     * Get the number of payables in the given pool.
51
     *
52
     * @param Pool $pool
53
     * @param Session $session
54
     *
55
     * @return int
56
     */
57
    public function getPayableCount(Pool $pool, Session $session): int
58
    {
59
        return $this->getQuery($pool, $session)->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getQuery($pool, $session)->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...
60
    }
61
62
    /**
63
     * Get the number of remitments in the given pool.
64
     *
65
     * @param Pool $pool
66
     * @param Session $session
67
     *
68
     * @return int
69
     */
70
    public function getRemitmentCount(Pool $pool, Session $session): int
71
    {
72
        return $this->getQuery($pool, $session)->whereHas('remitment')->count();
73
    }
74
75
    /**
76
     * @param Pool $pool
77
     * @param Session $session
78
     *
79
     * @return int
80
     */
81
    public function getRemitmentAmount(Pool $pool, Session $session): int
82
    {
83
        $count = $this->getRemitmentCount($pool, $session);
84
        if(!$pool->deposit_fixed)
85
        {
86
            // Sum the amounts for all deposits
87
            return $count === 0 ? 0 :
88
                $this->balanceCalculator->getPoolDepositAmount($pool, $session);
89
        }
90
91
        return $count * $this->balanceCalculator->getPayableAmount($pool, $session);
92
    }
93
94
    /**
95
     * @param Pool $pool
96
     * @param Session $session
97
     * @param int $page
98
     *
99
     * @return Collection
100
     */
101
    public function getPayables(Pool $pool, Session $session, int $page = 0): Collection
102
    {
103
        $amount = $this->balanceCalculator->getPayableAmount($pool, $session);
104
        $payables = $this->getQuery($pool, $session)
105
            ->select('payables.*')
106
            ->addSelect(DB::raw('member_defs.name as member'))
107
            ->join('members', 'members.id', '=', 'subscriptions.member_id')
108
            ->join('member_defs', 'members.def_id', '=', 'member_defs.id')
109
            ->with(['remitment', 'remitment.auction'])
110
            ->page($page, $this->tenantService->getLimit())
111
            ->get()
112
            ->each(function($payable) use($amount) {
113
                $payable->amount = $amount;
114
            });
115
        if(!$pool->remit_planned)
116
        {
117
            return $payables;
118
        }
119
120
        // When the number of remitments is planned, the list is padded to the expected number.
121
        $remitmentCount = $this->summaryService->getSessionRemitmentCount($pool, $session);
122
        $emptyPayable = (object)[
123
            'id' => 0,
124
            'member' => trans('tontine.remitment.labels.not-assigned'),
125
            'amount' => $amount,
126
            'remitment' => null,
127
        ];
128
129
        return $payables->pad($remitmentCount, $emptyPayable);
130
    }
131
132
    /**
133
     * Find the unique payable for a pool and a session.
134
     *
135
     * @param Pool $pool The pool
136
     * @param Session $session The session
137
     * @param int $payableId
138
     *
139
     * @return Payable|null
140
     */
141
    public function getPayable(Pool $pool, Session $session, int $payableId): ?Payable
142
    {
143
        return $this->getQuery($pool, $session)
144
            ->with(['remitment', 'remitment.auction'])
145
            ->select('payables.*')
146
            ->find($payableId);
147
    }
148
149
    /**
150
     * Create a remitment.
151
     *
152
     * @param Pool $pool The pool
153
     * @param Session $session The session
154
     * @param int $payableId
155
     *
156
     * @return void
157
     */
158
    public function savePlannedRemitment(Pool $pool, Session $session, int $payableId): void
159
    {
160
        if(!$pool->remit_planned || $pool->remit_auction)
161
        {
162
            // Only when remitments are planned and without auctions.
163
            return;
164
        }
165
        // The payable is supposed to already have been associated to the session.
166
        $payable = $this->getPayable($pool, $session, $payableId);
167
        if(!$payable)
168
        {
169
            throw new MessageException(trans('tontine.subscription.errors.not_found'));
170
        }
171
        if($payable->remitment)
172
        {
173
            return;
174
        }
175
        $payable->remitment()->create([]);
176
    }
177
178
    /**
179
     * Create a remitment.
180
     *
181
     * @param Pool $pool The pool
182
     * @param Session $session The session
183
     * @param int $payableId
184
     * @param int $auction
185
     *
186
     * @return void
187
     */
188
    public function saveRemitment(Pool $pool, Session $session, int $payableId, int $auction): void
189
    {
190
        // Cannot use the getPayable() method here,
191
        // because there's no session attached to the payable.
192
        $payable = Payable::with(['subscription.member'])
193
            ->whereDoesntHave('remitment')
194
            ->whereIn('subscription_id', $pool->subscriptions()->pluck('id'))
195
            ->find($payableId);
196
        if(!$payable)
197
        {
198
            throw new MessageException(trans('tontine.subscription.errors.not_found'));
199
        }
200
        if($payable->session_id !== null && $payable->session_id !== $session->id)
0 ignored issues
show
Bug introduced by
The property session_id does not seem to exist on Illuminate\Database\Eloq...gHasThroughRelationship.
Loading history...
201
        {
202
            // The selected member is already planned on another session.
203
            throw new MessageException(trans('tontine.remitment.errors.planning'));
204
        }
205
        // If the pool remitments are planned, then check the payable count for the session.
206
        if($pool->remit_planned)
207
        {
208
            $plannedCount = $this->summaryService->getSessionRemitmentCount($pool, $session);
209
            $remittedCount = $this->getRemitmentCount($pool, $session);
210
            if($remittedCount >= $plannedCount)
211
            {
212
                throw new MessageException(trans('tontine.remitment.errors.max-count'));
213
            }
214
        }
215
216
        DB::transaction(function() use($pool, $session, $payable, $auction) {
217
            // Associate the payable with the session.
218
            $payable->session()->associate($session);
219
            $payable->save();
220
            // Create the remitment.
221
            $remitment = $payable->remitment()->create([]);
222
223
            if($pool->remit_auction && $auction > 0)
224
            {
225
                // Create the corresponding auction.
226
                Auction::create([
227
                    'amount' => $auction,
228
                    'paid' => true, // The auction is supposed to have been immediatly paid.
229
                    'session_id' => $session->id,
230
                    'remitment_id' => $remitment->id,
231
                ]);
232
            }
233
        });
234
    }
235
236
    /**
237
     * Delete a remitment.
238
     *
239
     * @param Pool $pool The pool
240
     * @param Session $session The session
241
     * @param int $payableId
242
     *
243
     * @return void
244
     */
245
    public function deleteRemitment(Pool $pool, Session $session, int $payableId): void
246
    {
247
        $payable = $this->getQuery($pool, $session)
248
            ->with(['remitment', 'remitment.auction'])
249
            ->select('payables.*')
250
            ->find($payableId);
251
        if(!$payable || !($remitment = $payable->remitment))
252
        {
253
            return;
254
        }
255
256
        DB::transaction(function() use($pool, $payable, $remitment) {
257
            // Delete the corresponding auction, in case there was one on the remitment.
258
            $remitment->auction()->delete();
259
            $remitment->delete();
260
            // Detach from the session, but only if the remitment was not planned.
261
            if(!$pool->remit_planned || $pool->remit_auction)
262
            {
263
                $payable->session()->dissociate();
264
                $payable->save();
265
            }
266
        });
267
    }
268
269
    /**
270
     * Get the unpaid subscriptions of a given pool.
271
     *
272
     * @param Pool $pool
273
     * @param Session $session The session
274
     *
275
     * @return Collection
276
     */
277
    public function getSubscriptions(Pool $pool, Session $session): Collection
278
    {
279
        $subscriptions = $pool->subscriptions()
280
            ->join('members', 'subscriptions.member_id', '=', 'members.id')
281
            ->join('member_defs', 'members.def_id', '=', 'member_defs.id')
282
            ->orderBy('member_defs.name', 'asc')
283
            ->orderBy('subscriptions.id', 'asc')
284
            ->with(['payable', 'member'])
285
            ->whereHas('payable', fn($query) => $query->whereDoesntHave('remitment'))
286
            ->select('subscriptions.*')
287
            ->get();
288
        if($pool->remit_planned && !$pool->remit_auction)
289
        {
290
            // Only the beneficiaries that are not yet planned.
291
            return $subscriptions
292
                ->filter(fn($subscription) => !$subscription->payable->session_id)
293
                ->pluck('member.name', 'payable.id');
294
        }
295
        // Also return the beneficiaries that have not yet been remitted.
296
        return $subscriptions
297
            ->filter(fn($subscription) => !$subscription->payable->session_id ||
298
                $subscription->payable->session_id === $session->id)
299
            ->pluck('member.name', 'payable.id');
300
    }
301
}
302