RefundManager::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 1
b 0
f 0
nc 1
nop 5
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright (C) 2020-2025 Iain Cambridge
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU LESSER GENERAL PUBLIC LICENSE as published by
10
 * the Free Software Foundation, either version 2.1 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
22
namespace Parthenon\Billing\Refund;
23
24
use Brick\Math\RoundingMode;
25
use Brick\Money\Currency;
26
use Brick\Money\Money;
27
use Obol\Model\Refund\IssueRefund;
28
use Obol\Provider\ProviderInterface;
29
use Parthenon\Billing\Entity\BillingAdminInterface;
30
use Parthenon\Billing\Entity\Payment;
31
use Parthenon\Billing\Entity\Refund;
32
use Parthenon\Billing\Entity\Subscription;
33
use Parthenon\Billing\Enum\PaymentStatus;
34
use Parthenon\Billing\Enum\RefundStatus;
35
use Parthenon\Billing\Event\RefundCreated;
36
use Parthenon\Billing\Exception\RefundLimitExceededException;
37
use Parthenon\Billing\Factory\EntityFactoryInterface;
38
use Parthenon\Billing\Repository\PaymentRepositoryInterface;
39
use Parthenon\Billing\Repository\RefundRepositoryInterface;
40
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
41
42
class RefundManager implements RefundManagerInterface
43
{
44
    public function __construct(
45
        private ProviderInterface $provider,
46
        private PaymentRepositoryInterface $paymentRepository,
47
        private RefundRepositoryInterface $refundRepository,
48
        private EventDispatcherInterface $dispatcher,
49
        private EntityFactoryInterface $entityFactory,
50
    ) {
51
    }
52
53
    public function issueRefundForPayment(Payment $payment, Money $amount, ?BillingAdminInterface $billingAdmin = null, ?string $reason = null): Refund
54
    {
55
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
56
57
        if ($totalRefunded->isGreaterThanOrEqualTo($payment->getMoneyAmount())) {
58
            throw new RefundLimitExceededException('Payment has already been fully refunded');
59
        }
60
61
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
62
63
        if ($amount->isGreaterThan($refundable)) {
64
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
65
        }
66
67
        return $this->handleRefund($amount, $payment, $totalRefunded, $billingAdmin, $reason);
68
    }
69
70
    public function issueFullRefundForSubscription(Subscription $subscription, ?BillingAdminInterface $billingAdmin = null): Refund
71
    {
72
        $payment = $this->paymentRepository->getLastPaymentForSubscription($subscription);
73
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
74
75
        $amount = $subscription->getMoneyAmount();
76
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
77
78
        if ($amount->isGreaterThan($refundable)) {
79
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
80
        }
81
82
        return $this->handleRefund($amount, $payment, $totalRefunded, $billingAdmin);
83
    }
84
85
    public function issueProrateRefundForSubscription(Subscription $subscription, ?BillingAdminInterface $billingAdmin, \DateTimeInterface $start, \DateTimeInterface $end): Refund
86
    {
87
        if ('month' === $subscription->getPaymentSchedule()) {
88
            $days = date('t');
89
        } elseif ('year' === $subscription->getPaymentSchedule()) {
90
            $days = 365;
91
        } else {
92
            $days = 7;
93
        }
94
95
        $interval = $start->diff($end);
96
        if (!is_int($interval->days)) {
97
            throw new \Exception('Invalid diff');
98
        }
99
100
        $payment = $this->paymentRepository->getLastPaymentForSubscription($subscription);
101
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
102
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
103
104
        $perDay = $subscription->getMoneyAmount()->dividedBy($days, RoundingMode::HALF_UP);
105
        $totalAmount = $perDay->multipliedBy(abs($interval->days), RoundingMode::HALF_UP)->multipliedBy($subscription->getSeats(), RoundingMode::HALF_UP);
106
107
        if ($totalAmount->isGreaterThan($refundable)) {
108
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
109
        }
110
111
        return $this->handleRefund($totalAmount, $payment, $totalRefunded, $billingAdmin);
112
    }
113
114
    protected function createEntityRecord(\Obol\Model\Refund $refund, ?BillingAdminInterface $billingAdmin, Payment $payment, ?string $reason = null): Refund
115
    {
116
        $money = Money::ofMinor($refund->getAmount(), Currency::of(strtoupper($refund->getCurrency())));
117
        if ($payment->getMoneyAmount()->isEqualTo($money)) {
118
            $payment->setStatus(PaymentStatus::FULLY_REFUNDED);
119
        } else {
120
            $payment->setStatus(PaymentStatus::PARTIALLY_REFUNDED);
121
        }
122
        $this->paymentRepository->save($payment);
123
        $refundEn = $this->entityFactory->getRefundEntity();
124
        $refundEn->setAmount($refund->getAmount());
125
        $refundEn->setCurrency(strtoupper($refund->getCurrency()));
126
        $refundEn->setExternalReference($refund->getId());
127
        $refundEn->setStatus(RefundStatus::ISSUED);
128
        $refundEn->setBillingAdmin($billingAdmin);
129
        $refundEn->setPayment($payment);
130
        $refundEn->setCustomer($payment->getCustomer());
0 ignored issues
show
Bug introduced by
It seems like $payment->getCustomer() can also be of type null; however, parameter $customer of Parthenon\Billing\Entity\Refund::setCustomer() does only seem to accept Parthenon\Billing\Entity\CustomerInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

130
        $refundEn->setCustomer(/** @scrutinizer ignore-type */ $payment->getCustomer());
Loading history...
131
        $refundEn->setCreatedAt(new \DateTime());
132
        $refundEn->setReason($reason);
133
134
        $this->refundRepository->save($refundEn);
135
        $this->dispatcher->dispatch(new RefundCreated($refundEn), RefundCreated::NAME);
136
137
        return $refundEn;
138
    }
139
140
    /**
141
     * @throws \Brick\Money\Exception\MoneyMismatchException
142
     * @throws \Obol\Exception\UnsupportedFunctionalityException
143
     */
144
    protected function handleRefund(Money $amount, Payment $payment, Money $totalRefunded, ?BillingAdminInterface $billingAdmin, ?string $reason = null): Refund
145
    {
146
        $issueRefund = new IssueRefund();
147
        $issueRefund->setAmount($amount);
148
        $issueRefund->setPaymentExternalReference($payment->getPaymentReference());
149
150
        $refund = $this->provider->refunds()->issueRefund($issueRefund);
151
152
        $totalRefunded = $totalRefunded->plus($amount);
153
154
        if ($totalRefunded->isEqualTo($payment->getAmount())) {
155
            $payment->setStatus(PaymentStatus::FULLY_REFUNDED);
156
        } else {
157
            $payment->setStatus(PaymentStatus::PARTIALLY_REFUNDED);
158
        }
159
160
        return $this->createEntityRecord($refund, $billingAdmin, $payment, $reason);
161
    }
162
}
163