SubscriptionManager::startSubscription()   C
last analyzed

Complexity

Conditions 10
Paths 145

Size

Total Lines 78
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
cc 10
eloc 51
c 10
b 0
f 0
nc 145
nop 7
dl 0
loc 78
rs 6.9024

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Subscription;
23
24
use Obol\Model\CancelSubscription;
25
use Obol\Model\Enum\ProrataType;
26
use Obol\Provider\ProviderInterface;
27
use Parthenon\Billing\Dto\StartSubscriptionDto;
28
use Parthenon\Billing\Entity\CustomerInterface;
29
use Parthenon\Billing\Entity\PaymentCard;
30
use Parthenon\Billing\Entity\Price;
31
use Parthenon\Billing\Entity\Subscription;
32
use Parthenon\Billing\Entity\SubscriptionPlan;
33
use Parthenon\Billing\Enum\BillingChangeTiming;
34
use Parthenon\Billing\Enum\SubscriptionStatus;
35
use Parthenon\Billing\Event\PaymentCreated;
36
use Parthenon\Billing\Event\SubscriptionCancelled;
37
use Parthenon\Billing\Event\SubscriptionCreated;
38
use Parthenon\Billing\Exception\SubscriptionCreationException;
39
use Parthenon\Billing\Factory\EntityFactoryInterface;
40
use Parthenon\Billing\Obol\BillingDetailsFactoryInterface;
41
use Parthenon\Billing\Obol\PaymentFactoryInterface;
42
use Parthenon\Billing\Obol\SubscriptionFactoryInterface;
43
use Parthenon\Billing\Plan\Plan;
44
use Parthenon\Billing\Plan\PlanManagerInterface;
45
use Parthenon\Billing\Plan\PlanPrice;
46
use Parthenon\Billing\Repository\PaymentCardRepositoryInterface;
47
use Parthenon\Billing\Repository\PaymentRepositoryInterface;
48
use Parthenon\Billing\Repository\PriceRepositoryInterface;
49
use Parthenon\Billing\Repository\SubscriptionPlanRepositoryInterface;
50
use Parthenon\Billing\Repository\SubscriptionRepositoryInterface;
51
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
52
53
final class SubscriptionManager implements SubscriptionManagerInterface
54
{
55
    public function __construct(
56
        private PaymentCardRepositoryInterface $paymentDetailsRepository,
57
        private ProviderInterface $provider,
58
        private BillingDetailsFactoryInterface $billingDetailsFactory,
59
        private PaymentFactoryInterface $paymentFactory,
60
        private SubscriptionFactoryInterface $subscriptionFactory,
61
        private PaymentRepositoryInterface $paymentRepository,
62
        private PlanManagerInterface $planManager,
63
        private SubscriptionPlanRepositoryInterface $subscriptionPlanRepository,
64
        private PriceRepositoryInterface $priceRepository,
65
        private SubscriptionRepositoryInterface $subscriptionRepository,
66
        private EntityFactoryInterface $entityFactory,
67
        private EventDispatcherInterface $dispatcher,
68
    ) {
69
    }
70
71
    public function startSubscription(
72
        CustomerInterface $customer,
73
        SubscriptionPlan|Plan $plan,
74
        Price|PlanPrice $planPrice,
75
        ?PaymentCard $paymentDetails = null,
76
        int $seatNumbers = 1,
77
        ?bool $hasTrial = null,
78
        ?int $trialLengthDays = null,
79
    ): Subscription {
80
        $billingDetails = $this->billingDetailsFactory->createFromCustomerAndPaymentDetails($customer, $paymentDetails);
0 ignored issues
show
Bug introduced by
It seems like $paymentDetails can also be of type null; however, parameter $paymentDetails of Parthenon\Billing\Obol\B...omerAndPaymentDetails() does only seem to accept Parthenon\Billing\Entity\PaymentCard, 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
        $billingDetails = $this->billingDetailsFactory->createFromCustomerAndPaymentDetails($customer, /** @scrutinizer ignore-type */ $paymentDetails);
Loading history...
81
        $obolSubscription = $this->subscriptionFactory->createSubscription($billingDetails, $planPrice, $seatNumbers, $hasTrial ?? $plan->getHasTrial(), $trialLengthDays ?? $plan->getTrialLengthDays());
82
        $obolSubscription->setStoredPaymentReference($paymentDetails->getStoredPaymentReference());
0 ignored issues
show
Bug introduced by
The method getStoredPaymentReference() does not exist on null. ( Ignorable by Annotation )

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

82
        $obolSubscription->setStoredPaymentReference($paymentDetails->/** @scrutinizer ignore-call */ getStoredPaymentReference());

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...
83
84
        if ($this->subscriptionRepository->hasActiveSubscription($customer)) {
85
            $subscription = $this->subscriptionRepository->getOneActiveSubscriptionForCustomer($customer);
86
87
            if ($subscription->getCurrency() != $planPrice->getCurrency()) {
88
                throw new SubscriptionCreationException("Can't add a child subscription for a different currency");
89
            }
90
91
            $obolSubscription->setParentReference($subscription->getMainExternalReference());
92
        }
93
94
        $subscriptionCreationResponse = $this->provider->payments()->startSubscription($obolSubscription);
95
        if ($subscriptionCreationResponse->hasCustomerCreation()) {
96
            $customer->setPaymentProviderDetailsUrl($subscriptionCreationResponse->getCustomerCreation()->getDetailsUrl());
97
            $customer->setExternalCustomerReference($subscriptionCreationResponse->getCustomerCreation()->getReference());
98
        }
99
100
        $subscription = $this->entityFactory->getSubscriptionEntity();
101
        $subscription->setPlanName($plan->getName());
102
        if ($planPrice->isRecurring()) {
0 ignored issues
show
Bug introduced by
The method isRecurring() does not exist on Parthenon\Billing\Plan\PlanPrice. ( Ignorable by Annotation )

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

102
        if ($planPrice->/** @scrutinizer ignore-call */ isRecurring()) {

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...
103
            $subscription->setPaymentSchedule($planPrice->getSchedule());
104
        }
105
        $subscription->setActive(true);
106
        $subscription->setMoneyAmount($subscriptionCreationResponse->getPaymentDetails()?->getAmount());
107
        $subscription->setStatus(SubscriptionStatus::ACTIVE);
108
        $subscription->setMainExternalReference($subscriptionCreationResponse->getSubscriptionId());
109
        $subscription->setChildExternalReference($subscriptionCreationResponse->getLineId());
110
        $subscription->setSeats($seatNumbers);
111
        $subscription->setCreatedAt(new \DateTime());
112
        $subscription->setUpdatedAt(new \DateTime());
113
        $subscription->setStartOfCurrentPeriod(new \DateTime());
114
        $subscription->setValidUntil($subscriptionCreationResponse->getBilledUntil());
115
        $subscription->setCustomer($customer);
116
        $subscription->setMainExternalReferenceDetailsUrl($subscriptionCreationResponse->getDetailsUrl());
117
        $subscription->setPaymentDetails($paymentDetails);
118
        $subscription->setTrialLengthDays($obolSubscription->getTrialLengthDays());
119
        $subscription->setHasTrial($obolSubscription->hasTrial());
120
121
        if ($plan instanceof SubscriptionPlan) {
122
            $subscription->setSubscriptionPlan($plan);
123
        } elseif ($plan->hasEntityId()) {
124
            $subscriptionPlan = $this->subscriptionPlanRepository->findById($plan->getEntityId());
125
            $subscription->setSubscriptionPlan($subscriptionPlan);
126
        }
127
128
        if ($planPrice instanceof Price) {
129
            $subscription->setPrice($planPrice);
130
        } elseif ($planPrice->hasEntityId()) {
131
            $price = $this->priceRepository->findById($planPrice->getEntityId());
132
            $subscription->setPrice($price);
133
        }
134
        $this->subscriptionRepository->save($subscription);
135
        $this->subscriptionRepository->updateValidUntilForAllActiveSubscriptions($customer, $subscription->getMainExternalReference(), $subscriptionCreationResponse->getBilledUntil());
136
137
        $this->dispatcher->dispatch(new SubscriptionCreated($subscription), SubscriptionCreated::NAME);
138
139
        $obolPaymentDetails = $subscriptionCreationResponse->getPaymentDetails();
140
        if ($obolPaymentDetails) {
141
            $payment = $this->paymentFactory->fromSubscriptionCreation($obolPaymentDetails, $customer);
142
            $payment->addSubscription($subscription);
143
            $this->paymentRepository->save($payment);
144
145
            $this->dispatcher->dispatch(new PaymentCreated($payment, true), PaymentCreated::NAME);
146
        }
147
148
        return $subscription;
149
    }
150
151
    public function startSubscriptionWithDto(CustomerInterface $customer, StartSubscriptionDto $startSubscriptionDto): Subscription
152
    {
153
        if (!$startSubscriptionDto->getPaymentDetailsId()) {
154
            $paymentDetails = $this->paymentDetailsRepository->getDefaultPaymentCardForCustomer($customer);
155
        } else {
156
            $paymentDetails = $this->paymentDetailsRepository->findById($startSubscriptionDto->getPaymentDetailsId());
157
        }
158
159
        $plan = $this->planManager->getPlanByName($startSubscriptionDto->getPlanName());
160
        $planPrice = $plan->getPriceForPaymentSchedule($startSubscriptionDto->getSchedule(), $startSubscriptionDto->getCurrency());
161
162
        return $this->startSubscription($customer, $plan, $planPrice, $paymentDetails, $startSubscriptionDto->getSeatNumbers());
163
    }
164
165
    public function cancelSubscriptionAtEndOfCurrentPeriod(Subscription $subscription): Subscription
166
    {
167
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription, false);
0 ignored issues
show
Unused Code introduced by
The call to Parthenon\Billing\Obol\S...ubscriptionFromEntity() has too many arguments starting with false. ( Ignorable by Annotation )

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

167
        /** @scrutinizer ignore-call */ 
168
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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(SubscriptionStatus::PENDING_CANCEL);
176
        $subscription->endAtEndOfPeriod();
177
178
        $this->subscriptionRepository->save($subscription);
179
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
180
181
        return $subscription;
182
    }
183
184
    public function cancelSubscriptionInstantly(Subscription $subscription): Subscription
185
    {
186
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
187
188
        $cancelRequest = new CancelSubscription();
189
        $cancelRequest->setSubscription($obolSubscription);
190
        $cancelRequest->setInstantCancel(true);
191
192
        $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...
193
194
        $subscription->setStatus(SubscriptionStatus::CANCELLED);
195
        $subscription->setActive(false);
196
        $subscription->endNow();
197
198
        $this->subscriptionRepository->save($subscription);
199
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
200
201
        return $subscription;
202
    }
203
204
    public function cancelSubscriptionOnDate(Subscription $subscription, \DateTimeInterface $dateTime): Subscription
205
    {
206
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
207
208
        $cancelRequest = new CancelSubscription();
209
        $cancelRequest->setSubscription($obolSubscription);
210
        $cancelRequest->setInstantCancel(false);
211
212
        $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...
213
214
        $subscription->setStatus(SubscriptionStatus::PENDING_CANCEL);
215
        $subscription->setEndedAt($dateTime);
216
        $subscription->setValidUntil($dateTime);
217
218
        $this->subscriptionRepository->save($subscription);
219
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
220
221
        return $subscription;
222
    }
223
224
    public function changeSubscriptionPrice(Subscription $subscription, Price $price, BillingChangeTiming $billingChangeTiming): void
225
    {
226
        $subscription->setPrice($price);
227
        $subscription->setMoneyAmount($price->getAsMoney());
228
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
229
        $obolSubscription->setPriceId($price->getExternalReference());
0 ignored issues
show
Bug introduced by
It seems like $price->getExternalReference() can also be of type null; however, parameter $priceId of Obol\Model\Subscription::setPriceId() 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

229
        $obolSubscription->setPriceId(/** @scrutinizer ignore-type */ $price->getExternalReference());
Loading history...
230
        $prorataTye = match ($billingChangeTiming) {
231
            BillingChangeTiming::INSTANTLY => ProrataType::NOW,
232
            default => ProrataType::NONE,
233
        };
234
235
        $this->provider->subscriptions()->updatePrice($obolSubscription, $prorataTye);
236
    }
237
238
    public function changeSubscriptionPlan(Subscription $subscription, SubscriptionPlan|Plan $plan, Price|PlanPrice $price, BillingChangeTiming $billingChangeTiming): void
239
    {
240
        $subscription->setSubscriptionPlan($plan);
0 ignored issues
show
Bug introduced by
It seems like $plan can also be of type Parthenon\Billing\Plan\Plan; however, parameter $subscriptionPlan of Parthenon\Billing\Entity...::setSubscriptionPlan() does only seem to accept Parthenon\Billing\Entity\SubscriptionPlan|null, 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

240
        $subscription->setSubscriptionPlan(/** @scrutinizer ignore-type */ $plan);
Loading history...
241
        $subscription->setPlanName($plan->getName());
242
        $subscription->setPrice($price);
0 ignored issues
show
Bug introduced by
It seems like $price can also be of type Parthenon\Billing\Plan\PlanPrice; however, parameter $price of Parthenon\Billing\Entity\Subscription::setPrice() does only seem to accept Parthenon\Billing\Entity\Price|null, 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

242
        $subscription->setPrice(/** @scrutinizer ignore-type */ $price);
Loading history...
243
        $subscription->setMoneyAmount($price->getAsMoney());
244
245
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
246
        $obolSubscription->setPriceId($price->getExternalReference());
0 ignored issues
show
Bug introduced by
The method getExternalReference() does not exist on Parthenon\Billing\Plan\PlanPrice. ( Ignorable by Annotation )

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

246
        $obolSubscription->setPriceId($price->/** @scrutinizer ignore-call */ getExternalReference());

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...
Bug introduced by
It seems like $price->getExternalReference() can also be of type null; however, parameter $priceId of Obol\Model\Subscription::setPriceId() 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

246
        $obolSubscription->setPriceId(/** @scrutinizer ignore-type */ $price->getExternalReference());
Loading history...
247
        $prorataTye = match ($billingChangeTiming) {
248
            BillingChangeTiming::INSTANTLY => ProrataType::NOW,
249
            default => ProrataType::NONE,
250
        };
251
252
        $this->provider->subscriptions()->updatePrice($obolSubscription, $prorataTye);
253
    }
254
}
255