Passed
Pull Request — main (#62)
by Thierry
05:23
created

PartialRefundService::getUnpaidDebtCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Siak\Tontine\Service\Meeting\Credit;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Relations\Relation;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\Log;
9
use Siak\Tontine\Exception\MessageException;
10
use Siak\Tontine\Model\Debt;
11
use Siak\Tontine\Model\Fund;
12
use Siak\Tontine\Model\PartialRefund;
13
use Siak\Tontine\Model\Session;
14
use Siak\Tontine\Service\LocaleService;
15
use Siak\Tontine\Service\Meeting\PaymentServiceInterface;
16
use Siak\Tontine\Service\TenantService;
17
use Siak\Tontine\Service\Tontine\FundService;
18
19
use function trans;
20
21
class PartialRefundService
22
{
23
    use RefundTrait;
0 ignored issues
show
introduced by
The trait Siak\Tontine\Service\Meeting\Credit\RefundTrait requires some properties which are not provided by Siak\Tontine\Service\Mee...it\PartialRefundService: $principal_debt, $is_principal, $session_id, $refund, $opened, $loan, $interest_debt, $id, $start_at, $partial_refunds, $recurrent_interest, $session, $is_interest
Loading history...
24
25
    /**
26
     * @param DebtCalculator $debtCalculator
27
     * @param TenantService $tenantService
28
     * @param LocaleService $localeService
29
     * @param FundService $fundService
30
     * @param PaymentServiceInterface $paymentService;
31
     */
32
    public function __construct(private DebtCalculator $debtCalculator,
33
        TenantService $tenantService, private LocaleService $localeService,
34
        FundService $fundService, private PaymentServiceInterface $paymentService)
35
    {
36
        $this->tenantService = $tenantService;
37
        $this->fundService = $fundService;
38
    }
39
40
    /**
41
     * @param Session $session The session
42
     * @param Fund $fund
43
     *
44
     * @return Builder|Relation
45
     */
46
    private function getQuery(Session $session, Fund $fund): Builder|Relation
47
    {
48
        return $session->partial_refunds()
49
            ->whereHas('debt', function(Builder|Relation $query) use($fund) {
50
                $query->whereHas('loan', function(Builder|Relation $query) use($fund) {
51
                    $query->where('fund_id', $fund->id);
52
                });
53
            });
54
    }
55
56
    /**
57
     * Get the number of partial refunds.
58
     *
59
     * @param Session $session The session
60
     * @param Fund $fund
61
     *
62
     * @return int
63
     */
64
    public function getPartialRefundCount(Session $session, Fund $fund): int
65
    {
66
        return $this->getQuery($session, $fund)->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getQuery($session, $fund)->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...
67
    }
68
69
    /**
70
     * Get the partial refunds.
71
     *
72
     * @param Session $session The session
73
     * @param Fund $fund
74
     * @param int $page
75
     *
76
     * @return Collection
77
     */
78
    public function getPartialRefunds(Session $session, Fund $fund, int $page = 0): Collection
79
    {
80
        return $this->getQuery($session, $fund)
81
            ->page($page, $this->tenantService->getLimit())
82
            ->with(['debt.refund', 'debt.loan.member', 'debt.loan.session'])
83
            ->orderBy('id')
84
            ->get()
85
            ->each(fn(PartialRefund $refund) => $refund->debtAmount =
0 ignored issues
show
Bug introduced by
The property debtAmount does not seem to exist on Siak\Tontine\Model\PartialRefund. 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...
86
                $this->debtCalculator->getDebtDueAmount($refund->debt, $session, false))
87
            ->sortBy('debt.loan.member.name', SORT_LOCALE_STRING)
88
            ->values();
89
    }
90
91
    /**
92
     * @param Session $session The session
93
     * @param Fund $fund
94
     * @param bool $with
95
     *
96
     * @return Builder|Relation
97
     */
98
    private function getUnpaidDebtsQuery(Session $session, Fund $fund, bool $with): Builder|Relation
99
    {
100
        return $this->getDebtsQuery($session, $fund, false, false)
101
            // A debt from a loan created in the current session can be refunded only
102
            // if it is an interest debt with fixed or unique interest.
103
            ->where(function(Builder $query) use($session) {
104
                $query->whereHas('loan', function(Builder $query) use($session) {
105
                    $query->where('session_id', '!=', $session->id);
106
                })->orWhere(function(Builder $query) {
107
                    $query->interest()
108
                        ->whereHas('loan', fn(Builder $q) => $q->fixedInterest());
109
                });
110
            })
111
            ->when($with, function(Builder $query) use($session) {
112
                $query->with([
113
                    'partial_refund' => fn($q) => $q->where('session_id', $session->id),
114
                ]);
115
            });
116
    }
117
118
    /**
119
     * Count the unpaid debts.
120
     *
121
     * @param Session $session The session
122
     * @param Fund $fund
123
     *
124
     * @return int
125
     */
126
    public function getUnpaidDebtCount(Session $session, Fund $fund): int
127
    {
128
        return $this->getUnpaidDebtsQuery($session, $fund, false)->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getUnpaidD... $fund, false)->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...
129
    }
130
131
    /**
132
     * Get the unpaid debts.
133
     *
134
     * @param Session $session The session
135
     * @param Fund $fund
136
     * @param int $page
137
     *
138
     * @return Collection
139
     */
140
    public function getUnpaidDebts(Session $session, Fund $fund, int $page = 0): Collection
141
    {
142
        return $this->getUnpaidDebtsQuery($session, $fund, true)
143
            ->page($page, $this->tenantService->getLimit())
144
            ->get();
145
    }
146
147
    /**
148
     * Get an unpaid debt.
149
     *
150
     * @param Session $session The session
151
     * @param Fund $fund
152
     *
153
     * @return Debt|null
154
     */
155
    public function getUnpaidDebt(Session $session, Fund $fund, int $debtId): ?Debt
156
    {
157
        return $this->getUnpaidDebtsQuery($session, $fund, true)->find($debtId);
158
    }
159
160
    /**
161
     * @param Session $session
162
     * @param Debt $debt
163
     *
164
     * @return bool
165
     */
166
    private function canPartiallyRefund(Session $session, Debt $debt): bool
167
    {
168
        return $session->opened && !$debt->refund;
169
    }
170
171
    /**
172
     * Create a refund.
173
     *
174
     * @param Session $session
175
     * @param Debt $debt
176
     * @param int $amount
177
     *
178
     * @return void
179
     */
180
    public function createPartialRefund(Session $session, Debt $debt, int $amount): void
181
    {
182
        if(!$this->canPartiallyRefund($session, $debt))
183
        {
184
            throw new MessageException(trans('meeting.refund.errors.cannot_create'));
185
        }
186
        // A partial refund must not totally refund a debt
187
        if($amount >= $this->debtCalculator->getDebtPayableAmount($debt, $session))
188
        {
189
            throw new MessageException(trans('meeting.refund.errors.pr_amount'));
190
        }
191
192
        $refund = new PartialRefund();
193
        $refund->amount = $amount;
194
        $refund->debt()->associate($debt);
195
        $refund->session()->associate($session);
196
        $refund->save();
197
    }
198
199
    /**
200
     * @param Session $session
201
     * @param PartialRefund $refund
202
     *
203
     * @return bool
204
     */
205
    private function canModifyPartialRefund(Session $session, PartialRefund $refund): bool
206
    {
207
        // A partial refund cannot be updated or deleted if the debt is already refunded.
208
        return $session->opened && $refund->debt->refund === null &&
209
            $this->paymentService->isEditable($refund);
210
    }
211
212
    /**
213
     * Find a refund.
214
     *
215
     * @param Session $session The session
216
     * @param int $refundId
217
     *
218
     * @return PartialRefund
219
     */
220
    public function getPartialRefund(Session $session, int $refundId): PartialRefund
221
    {
222
        $refund = PartialRefund::where('session_id', $session->id)
223
            ->with(['debt.refund'])
224
            ->find($refundId);
225
        if(!$refund)
226
        {
227
            throw new MessageException(trans('meeting.refund.errors.not_found'));
228
        }
229
230
        return $refund;
231
    }
232
233
    /**
234
     * Update a refund.
235
     *
236
     * @param Session $session The session
237
     * @param PartialRefund $refund
238
     * @param int $amount
239
     *
240
     * @return void
241
     */
242
    public function updatePartialRefund(Session $session, PartialRefund $refund, int $amount): void
243
    {
244
        if(!$this->canModifyPartialRefund($session, $refund))
245
        {
246
            throw new MessageException(trans('meeting.refund.errors.cannot_update'));
247
        }
248
        // A partial refund must not totally refund a debt
249
        $maxAmount = $refund->amount +
250
            $this->debtCalculator->getDebtPayableAmount($refund->debt, $session);
251
        if($amount >= $maxAmount)
252
        {
253
            throw new MessageException(trans('meeting.refund.errors.pr_amount'));
254
        }
255
256
        $refund->update(['amount' => $amount]);
257
    }
258
259
    /**
260
     * Delete a refund.
261
     *
262
     * @param Session $session The session
263
     * @param PartialRefund $refund
264
     *
265
     * @return void
266
     */
267
    public function deletePartialRefund(Session $session, PartialRefund $refund): void
268
    {
269
        if(!$this->canModifyPartialRefund($session, $refund))
270
        {
271
            throw new MessageException(trans('meeting.refund.errors.cannot_delete'));
272
        }
273
274
        $refund->delete();
275
    }
276
}
277