Passed
Push — main ( 0225e2...65d0e4 )
by Iain
16:39
created

RefundManager::issueRefundForPayment()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
nop 4
dl 0
loc 15
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright Iain Cambridge 2020-2023.
7
 *
8
 * Use of this software is governed by the Business Source License included in the LICENSE file and at https://getparthenon.com/docs/next/license.
9
 *
10
 * Change Date: TBD ( 3 years after 2.2.0 release )
11
 *
12
 * On the date above, in accordance with the Business Source License, use of this software will be governed by the open source license specified in the LICENSE file.
13
 */
14
15
namespace Parthenon\Billing\Refund;
16
17
use Brick\Math\RoundingMode;
18
use Brick\Money\Currency;
19
use Brick\Money\Money;
20
use Obol\Model\Refund\IssueRefund;
21
use Obol\Provider\ProviderInterface;
22
use Parthenon\Billing\Entity\BillingAdminInterface;
23
use Parthenon\Billing\Entity\Payment;
24
use Parthenon\Billing\Entity\Refund;
25
use Parthenon\Billing\Entity\Subscription;
26
use Parthenon\Billing\Enum\PaymentStatus;
27
use Parthenon\Billing\Enum\RefundStatus;
28
use Parthenon\Billing\Exception\RefundLimitExceededException;
29
use Parthenon\Billing\Repository\PaymentRepositoryInterface;
30
use Parthenon\Billing\Repository\RefundRepositoryInterface;
31
32
class RefundManager implements RefundManagerInterface
33
{
34
    public function __construct(
35
        private ProviderInterface $provider,
36
        private PaymentRepositoryInterface $paymentRepository,
37
        private RefundRepositoryInterface $refundRepository,
38
    ) {
39
    }
40
41
    public function issueRefundForPayment(Payment $payment, Money $amount, ?BillingAdminInterface $billingAdmin = null, ?string $comment = null): void
42
    {
43
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
44
45
        if ($totalRefunded->isGreaterThanOrEqualTo($payment->getMoneyAmount())) {
46
            throw new RefundLimitExceededException('Payment has already been fully refunded');
47
        }
48
49
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
50
51
        if ($amount->isGreaterThan($refundable)) {
52
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
53
        }
54
55
        $this->handleRefund($amount, $payment, $totalRefunded, $billingAdmin, $comment);
56
    }
57
58
    public function issueFullRefundForSubscription(Subscription $subscription, ?BillingAdminInterface $billingAdmin = null): void
59
    {
60
        $payment = $this->paymentRepository->getLastPaymentForSubscription($subscription);
61
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
62
63
        $amount = $subscription->getMoneyAmount();
64
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
65
66
        if ($amount->isGreaterThan($refundable)) {
67
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
68
        }
69
70
        $this->handleRefund($amount, $payment, $totalRefunded, $billingAdmin);
71
    }
72
73
    public function issueProrateRefundForSubscription(Subscription $subscription, ?BillingAdminInterface $billingAdmin, \DateTimeInterface $start, \DateTimeInterface $end): void
74
    {
75
        if ('month' === $subscription->getPaymentSchedule()) {
76
            $days = date('t');
77
        } elseif ('year' === $subscription->getPaymentSchedule()) {
78
            $days = 365;
79
        } else {
80
            $days = 7;
81
        }
82
83
        $interval = $start->diff($end);
84
        if (!is_int($interval->days)) {
85
            return;
86
        }
87
88
        $payment = $this->paymentRepository->getLastPaymentForSubscription($subscription);
89
        $totalRefunded = $this->refundRepository->getTotalRefundedForPayment($payment);
90
        $refundable = $payment->getMoneyAmount()->minus($totalRefunded);
91
92
        $perDay = $subscription->getMoneyAmount()->dividedBy($days, RoundingMode::HALF_UP);
93
        $totalAmount = $perDay->multipliedBy(abs($interval->days), RoundingMode::HALF_UP)->multipliedBy($subscription->getSeats(), RoundingMode::HALF_UP);
94
95
        if ($totalAmount->isGreaterThan($refundable)) {
96
            throw new RefundLimitExceededException('The refund amount is greater than the refundable amount for payment');
97
        }
98
99
        $this->handleRefund($totalAmount, $payment, $totalRefunded, $billingAdmin);
100
    }
101
102
    public function createEntityRecord(\Obol\Model\Refund $refund, ?BillingAdminInterface $billingAdmin, Payment $payment, ?string $comment = null): void
103
    {
104
        $money = Money::ofMinor($refund->getAmount(), Currency::of($refund->getCurrency()));
105
        if ($payment->getMoneyAmount()->isEqualTo($money)) {
106
            $payment->setStatus(PaymentStatus::FULLY_REFUNDED);
107
        } else {
108
            $payment->setStatus(PaymentStatus::PARTIALLY_REFUNDED);
109
        }
110
        $this->paymentRepository->save($payment);
111
        $refundEn = new Refund();
112
        $refundEn->setAmount($refund->getAmount());
113
        $refundEn->setCurrency($refund->getCurrency());
114
        $refundEn->setExternalReference($refund->getId());
115
        $refundEn->setStatus(RefundStatus::ISSUED);
116
        $refundEn->setBillingAdmin($billingAdmin);
117
        $refundEn->setPayment($payment);
118
        $refundEn->setCustomer($payment->getCustomer());
119
        $refundEn->setCreatedAt(new \DateTime());
120
        $refundEn->setReason($comment);
121
122
        $this->refundRepository->save($refundEn);
123
    }
124
125
    /**
126
     * @throws \Brick\Money\Exception\MoneyMismatchException
127
     * @throws \Obol\Exception\UnsupportedFunctionalityException
128
     */
129
    public function handleRefund(Money $amount, Payment $payment, Money $totalRefunded, ?BillingAdminInterface $billingAdmin, ?string $comment = null): void
130
    {
131
        $issueRefund = new IssueRefund();
132
        $issueRefund->setAmount($amount);
133
        $issueRefund->setPaymentExternalReference($payment->getPaymentReference());
134
135
        $refund = $this->provider->refunds()->issueRefund($issueRefund);
136
137
        $totalRefunded = $totalRefunded->plus($amount);
138
139
        if ($totalRefunded->isEqualTo($payment->getAmount())) {
140
            $payment->setStatus(PaymentStatus::FULLY_REFUNDED);
141
        } else {
142
            $payment->setStatus(PaymentStatus::PARTIALLY_REFUNDED);
143
        }
144
145
        $this->createEntityRecord($refund, $billingAdmin, $payment, $comment);
146
    }
147
}
148