Passed
Push — main ( b448d4...f7598c )
by Iain
04:19
created

SubscriptionManager::createSubscription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 5
dl 0
loc 2
rs 10
c 0
b 0
f 0
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\Subscription;
16
17
use Obol\Model\CancelSubscription;
18
use Obol\Model\Enum\RefundType;
19
use Obol\Provider\ProviderInterface;
20
use Parthenon\Billing\Dto\StartSubscriptionDto;
21
use Parthenon\Billing\Entity\CustomerInterface;
22
use Parthenon\Billing\Entity\EmbeddedSubscription;
23
use Parthenon\Billing\Entity\PaymentDetails;
24
use Parthenon\Billing\Entity\Price;
25
use Parthenon\Billing\Entity\Subscription;
26
use Parthenon\Billing\Entity\SubscriptionPlan;
27
use Parthenon\Billing\Exception\SubscriptionCreationException;
28
use Parthenon\Billing\Obol\BillingDetailsFactoryInterface;
29
use Parthenon\Billing\Obol\PaymentFactoryInterface;
30
use Parthenon\Billing\Obol\SubscriptionFactoryInterface;
31
use Parthenon\Billing\Plan\Plan;
32
use Parthenon\Billing\Plan\PlanManagerInterface;
33
use Parthenon\Billing\Plan\PlanPrice;
34
use Parthenon\Billing\Repository\PaymentDetailsRepositoryInterface;
35
use Parthenon\Billing\Repository\PaymentRepositoryInterface;
36
use Parthenon\Billing\Repository\PriceRepositoryInterface;
37
use Parthenon\Billing\Repository\SubscriptionPlanRepositoryInterface;
38
use Parthenon\Billing\Repository\SubscriptionRepositoryInterface;
39
40
final class SubscriptionManager implements SubscriptionManagerInterface
41
{
42
    public function __construct(
43
        private PaymentDetailsRepositoryInterface $paymentDetailsRepository,
44
        private ProviderInterface $provider,
45
        private BillingDetailsFactoryInterface $billingDetailsFactory,
46
        private PaymentFactoryInterface $paymentFactory,
47
        private SubscriptionFactoryInterface $subscriptionFactory,
48
        private PaymentRepositoryInterface $paymentRepository,
49
        private PlanManagerInterface $planManager,
50
        private SubscriptionPlanRepositoryInterface $subscriptionPlanRepository,
51
        private PriceRepositoryInterface $priceRepository,
52
        private SubscriptionRepositoryInterface $subscriptionRepository
53
    ) {
54
    }
55
56
    public function startSubscription(CustomerInterface $customer, SubscriptionPlan|Plan $plan, Price|PlanPrice $planPrice, PaymentDetails $paymentDetails, int $seatNumbers): Subscription
57
    {
58
        $billingDetails = $this->billingDetailsFactory->createFromCustomerAndPaymentDetails($customer, $paymentDetails);
59
        $obolSubscription = $this->subscriptionFactory->createSubscription($billingDetails, $planPrice, $seatNumbers);
0 ignored issues
show
Bug introduced by
It seems like $planPrice can also be of type Parthenon\Billing\Entity\Price; however, parameter $planPrice of Parthenon\Billing\Obol\S...e::createSubscription() does only seem to accept Parthenon\Billing\Plan\PlanPrice, 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

59
        $obolSubscription = $this->subscriptionFactory->createSubscription($billingDetails, /** @scrutinizer ignore-type */ $planPrice, $seatNumbers);
Loading history...
60
        $obolSubscription->setStoredPaymentReference($paymentDetails->getStoredPaymentReference());
61
62
        if ($this->subscriptionRepository->hasActiveSubscription($customer)) {
63
            $subscription = $this->subscriptionRepository->getOneActiveSubscriptionForCustomer($customer);
64
65
            if ($subscription->getCurrency() != $planPrice->getCurrency()) {
66
                throw new SubscriptionCreationException("Can't add a child subscription for a different currency");
67
            }
68
69
            $obolSubscription->setParentReference($subscription->getMainExternalReference());
70
        }
71
72
        $subscriptionCreationResponse = $this->provider->payments()->startSubscription($obolSubscription);
73
        if ($subscriptionCreationResponse->hasCustomerCreation()) {
74
            $customer->setPaymentProviderDetailsUrl($subscriptionCreationResponse->getCustomerCreation()->getDetailsUrl());
75
            $customer->setExternalCustomerReference($subscriptionCreationResponse->getCustomerCreation()->getReference());
76
        }
77
78
        $subscription = new Subscription();
79
        $subscription->setPlanName($plan->getName());
80
        $subscription->setPaymentSchedule($planPrice->getSchedule());
0 ignored issues
show
Bug introduced by
It seems like $planPrice->getSchedule() can also be of type null; however, parameter $paymentSchedule of Parthenon\Billing\Entity...n::setPaymentSchedule() does only seem to accept string, 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

80
        $subscription->setPaymentSchedule(/** @scrutinizer ignore-type */ $planPrice->getSchedule());
Loading history...
81
        $subscription->setActive(true);
82
        $subscription->setMoneyAmount($subscriptionCreationResponse->getPaymentDetails()->getAmount());
83
        $subscription->setStatus(\Parthenon\Billing\Entity\EmbeddedSubscription::STATUS_ACTIVE);
84
        $subscription->setMainExternalReference($subscriptionCreationResponse->getSubscriptionId());
85
        $subscription->setChildExternalReference($subscriptionCreationResponse->getLineId());
86
        $subscription->setSeats($seatNumbers);
87
        $subscription->setCreatedAt(new \DateTime());
88
        $subscription->setUpdatedAt(new \DateTime());
89
        $subscription->setStartOfCurrentPeriod(new \DateTime());
90
        $subscription->setValidUntil($subscriptionCreationResponse->getBilledUntil());
91
        $subscription->setCustomer($customer);
92
        $subscription->setMainExternalReferenceDetailsUrl($subscriptionCreationResponse->getDetailsUrl());
93
        $subscription->setPaymentExternalReference($subscriptionCreationResponse->getPaymentDetails()->getStoredPaymentReference());
94
95
        if ($plan instanceof SubscriptionPlan) {
96
            $subscription->setSubscriptionPlan($plan);
97
        } elseif ($plan->hasEntityId()) {
98
            $subscriptionPlan = $this->subscriptionPlanRepository->findById($plan->getEntityId());
99
            $subscription->setSubscriptionPlan($subscriptionPlan);
100
        }
101
102
        if ($planPrice instanceof Price) {
103
            $subscription->setPrice($planPrice);
104
        } elseif ($planPrice->hasEntityId()) {
105
            $price = $this->priceRepository->findById($planPrice->getEntityId());
106
            $subscription->setPrice($price);
107
        }
108
        $this->subscriptionRepository->save($subscription);
109
        $this->subscriptionRepository->updateValidUntilForAllActiveSubscriptions($customer, $subscription->getMainExternalReference(), $subscriptionCreationResponse->getBilledUntil());
110
111
        $payment = $this->paymentFactory->fromSubscriptionCreation($subscriptionCreationResponse, $customer);
112
        $payment->addSubscription($subscription);
113
        $this->paymentRepository->save($payment);
114
115
        return $subscription;
116
    }
117
118
    public function startSubscriptionWithDto(CustomerInterface $customer, StartSubscriptionDto $startSubscriptionDto): Subscription
119
    {
120
        if (!$startSubscriptionDto->getPaymentDetailsId()) {
121
            $paymentDetails = $this->paymentDetailsRepository->getDefaultPaymentDetailsForCustomer($customer);
122
        } else {
123
            $paymentDetails = $this->paymentDetailsRepository->findById($startSubscriptionDto->getPaymentDetailsId());
124
        }
125
126
        $plan = $this->planManager->getPlanByName($startSubscriptionDto->getPlanName());
127
        $planPrice = $plan->getPriceForPaymentSchedule($startSubscriptionDto->getSchedule(), $startSubscriptionDto->getCurrency());
128
129
        return $this->startSubscription($customer, $plan, $planPrice, $paymentDetails, $startSubscriptionDto->getSeatNumbers());
130
    }
131
132
    public function cancelSubscriptionAtEndOfCurrentPeriod(Subscription $subscription): Subscription
133
    {
134
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
135
136
        $cancelRequest = new CancelSubscription();
137
        $cancelRequest->setSubscription($obolSubscription);
138
        $cancelRequest->setInstantCancel(false);
139
        $cancelRequest->setRefundType(RefundType::NONE);
0 ignored issues
show
Bug introduced by
The method setRefundType() does not exist on Obol\Model\CancelSubscription. ( Ignorable by Annotation )

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

139
        $cancelRequest->/** @scrutinizer ignore-call */ 
140
                        setRefundType(RefundType::NONE);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
140
141
        $cancellation = $this->provider->payments()->stopSubscription($cancelRequest);
0 ignored issues
show
Unused Code introduced by
The assignment to $cancellation is dead and can be removed.
Loading history...
142
143
        $subscription->setStatus(EmbeddedSubscription::STATUS_CANCELLED);
144
        $subscription->endAtEndOfPeriod();
145
146
        return $subscription;
147
    }
148
149
    public function cancelSubscriptionInstantly(Subscription $subscription): Subscription
150
    {
151
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
152
153
        $cancelRequest = new CancelSubscription();
154
        $cancelRequest->setSubscription($obolSubscription);
155
        $cancelRequest->setInstantCancel(true);
156
157
        $cancellation = $this->provider->payments()->stopSubscription($cancelRequest);
0 ignored issues
show
Unused Code introduced by
The assignment to $cancellation is dead and can be removed.
Loading history...
158
159
        $subscription->setStatus(EmbeddedSubscription::STATUS_CANCELLED);
160
        $subscription->endNow();
161
162
        return $subscription;
163
    }
164
165
    public function cancelSubscriptionOnDate(Subscription $subscription, \DateTime $dateTime): Subscription
166
    {
167
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
168
169
        $cancelRequest = new CancelSubscription();
170
        $cancelRequest->setSubscription($obolSubscription);
171
        $cancelRequest->setInstantCancel(false);
172
173
        $cancellation = $this->provider->payments()->stopSubscription($cancelRequest);
0 ignored issues
show
Unused Code introduced by
The assignment to $cancellation is dead and can be removed.
Loading history...
174
175
        $subscription->setStatus(EmbeddedSubscription::STATUS_CANCELLED);
176
        $subscription->setEndedAt($dateTime);
177
        $subscription->setValidUntil($dateTime);
178
179
        return $subscription;
180
    }
181
182
    /**
183
     * @param Plan      $plan
184
     * @param PlanPrice $planPrice
185
     */
186
    public function createSubscription(SubscriptionPlan|Plan $plan, PlanPrice|Price $planPrice, \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, int $seatNumbers, CustomerInterface $customer): Subscription
0 ignored issues
show
Unused Code introduced by
The parameter $customer 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

186
    public function createSubscription(SubscriptionPlan|Plan $plan, PlanPrice|Price $planPrice, \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, int $seatNumbers, /** @scrutinizer ignore-unused */ CustomerInterface $customer): Subscription

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...
Unused Code introduced by
The parameter $planPrice 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

186
    public function createSubscription(SubscriptionPlan|Plan $plan, /** @scrutinizer ignore-unused */ PlanPrice|Price $planPrice, \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, int $seatNumbers, CustomerInterface $customer): Subscription

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...
Unused Code introduced by
The parameter $plan 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

186
    public function createSubscription(/** @scrutinizer ignore-unused */ SubscriptionPlan|Plan $plan, PlanPrice|Price $planPrice, \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, int $seatNumbers, CustomerInterface $customer): Subscription

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...
Unused Code introduced by
The parameter $seatNumbers 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

186
    public function createSubscription(SubscriptionPlan|Plan $plan, PlanPrice|Price $planPrice, \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, /** @scrutinizer ignore-unused */ int $seatNumbers, CustomerInterface $customer): Subscription

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...
Unused Code introduced by
The parameter $subscriptionCreationResponse 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

186
    public function createSubscription(SubscriptionPlan|Plan $plan, PlanPrice|Price $planPrice, /** @scrutinizer ignore-unused */ \Obol\Model\SubscriptionCreationResponse $subscriptionCreationResponse, int $seatNumbers, CustomerInterface $customer): Subscription

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...
187
    {
188
    }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Parthenon\Billing\Entity\Subscription. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
189
}
190