Passed
Push — main ( 3df85b...6e75fa )
by Iain
05:19
created

SubscriptionManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
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 12
dl 0
loc 14
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright Humbly Arrogant Software Limited 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: 26.06.2026 ( 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\ProrataType;
19
use Obol\Provider\ProviderInterface;
20
use Parthenon\Billing\Dto\StartSubscriptionDto;
21
use Parthenon\Billing\Entity\CustomerInterface;
22
use Parthenon\Billing\Entity\PaymentCard;
23
use Parthenon\Billing\Entity\Price;
24
use Parthenon\Billing\Entity\Subscription;
25
use Parthenon\Billing\Entity\SubscriptionPlan;
26
use Parthenon\Billing\Enum\BillingChangeTiming;
27
use Parthenon\Billing\Enum\SubscriptionStatus;
28
use Parthenon\Billing\Event\PaymentCreated;
29
use Parthenon\Billing\Event\SubscriptionCancelled;
30
use Parthenon\Billing\Event\SubscriptionCreated;
31
use Parthenon\Billing\Exception\SubscriptionCreationException;
32
use Parthenon\Billing\Factory\EntityFactoryInterface;
33
use Parthenon\Billing\Obol\BillingDetailsFactoryInterface;
34
use Parthenon\Billing\Obol\PaymentFactoryInterface;
35
use Parthenon\Billing\Obol\SubscriptionFactoryInterface;
36
use Parthenon\Billing\Plan\Plan;
37
use Parthenon\Billing\Plan\PlanManagerInterface;
38
use Parthenon\Billing\Plan\PlanPrice;
39
use Parthenon\Billing\Repository\PaymentCardRepositoryInterface;
40
use Parthenon\Billing\Repository\PaymentRepositoryInterface;
41
use Parthenon\Billing\Repository\PriceRepositoryInterface;
42
use Parthenon\Billing\Repository\SubscriptionPlanRepositoryInterface;
43
use Parthenon\Billing\Repository\SubscriptionRepositoryInterface;
44
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
45
46
final class SubscriptionManager implements SubscriptionManagerInterface
47
{
48
    public function __construct(
49
        private PaymentCardRepositoryInterface $paymentDetailsRepository,
50
        private ProviderInterface $provider,
51
        private BillingDetailsFactoryInterface $billingDetailsFactory,
52
        private PaymentFactoryInterface $paymentFactory,
53
        private SubscriptionFactoryInterface $subscriptionFactory,
54
        private PaymentRepositoryInterface $paymentRepository,
55
        private PlanManagerInterface $planManager,
56
        private SubscriptionPlanRepositoryInterface $subscriptionPlanRepository,
57
        private PriceRepositoryInterface $priceRepository,
58
        private SubscriptionRepositoryInterface $subscriptionRepository,
59
        private EntityFactoryInterface $entityFactory,
60
        private EventDispatcherInterface $dispatcher,
61
    ) {
62
    }
63
64
    public function createSubscription(CustomerInterface $customer, SubscriptionPlan|Plan $plan, Price|PlanPrice $planPrice): Subscription
65
    {
66
    }
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...
67
68
    public function startSubscription(CustomerInterface $customer, SubscriptionPlan|Plan $plan, Price|PlanPrice $planPrice, ?PaymentCard $paymentDetails = null, int $seatNumbers = 1, ?bool $hasTrial = null, ?int $trialLengthDays = 0): Subscription
69
    {
70
        $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

70
        $billingDetails = $this->billingDetailsFactory->createFromCustomerAndPaymentDetails($customer, /** @scrutinizer ignore-type */ $paymentDetails);
Loading history...
71
        $obolSubscription = $this->subscriptionFactory->createSubscription($billingDetails, $planPrice, $seatNumbers, $hasTrial ?? $plan->getHasTrial(), $trialLengthDays ?? $plan->getTrialLengthDays());
72
        $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

72
        $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...
73
74
        if ($this->subscriptionRepository->hasActiveSubscription($customer)) {
75
            $subscription = $this->subscriptionRepository->getOneActiveSubscriptionForCustomer($customer);
76
77
            if ($subscription->getCurrency() != $planPrice->getCurrency()) {
78
                throw new SubscriptionCreationException("Can't add a child subscription for a different currency");
79
            }
80
81
            $obolSubscription->setParentReference($subscription->getMainExternalReference());
82
        }
83
84
        $subscriptionCreationResponse = $this->provider->payments()->startSubscription($obolSubscription);
85
        if ($subscriptionCreationResponse->hasCustomerCreation()) {
86
            $customer->setPaymentProviderDetailsUrl($subscriptionCreationResponse->getCustomerCreation()->getDetailsUrl());
87
            $customer->setExternalCustomerReference($subscriptionCreationResponse->getCustomerCreation()->getReference());
88
        }
89
90
        $subscription = $this->entityFactory->getSubscriptionEntity();
91
        $subscription->setPlanName($plan->getName());
92
        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

92
        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...
93
            $subscription->setPaymentSchedule($planPrice->getSchedule());
94
        }
95
        $subscription->setActive(true);
96
        $subscription->setMoneyAmount($subscriptionCreationResponse->getPaymentDetails()?->getAmount());
97
        $subscription->setStatus(SubscriptionStatus::ACTIVE);
98
        $subscription->setMainExternalReference($subscriptionCreationResponse->getSubscriptionId());
99
        $subscription->setChildExternalReference($subscriptionCreationResponse->getLineId());
100
        $subscription->setSeats($seatNumbers);
101
        $subscription->setCreatedAt(new \DateTime());
102
        $subscription->setUpdatedAt(new \DateTime());
103
        $subscription->setStartOfCurrentPeriod(new \DateTime());
104
        $subscription->setValidUntil($subscriptionCreationResponse->getBilledUntil());
105
        $subscription->setCustomer($customer);
106
        $subscription->setMainExternalReferenceDetailsUrl($subscriptionCreationResponse->getDetailsUrl());
107
        $subscription->setPaymentDetails($paymentDetails);
108
        $subscription->setTrialLengthDays($obolSubscription->getTrialLengthDays());
109
        $subscription->setHasTrial($obolSubscription->hasTrial());
110
111
        if ($plan instanceof SubscriptionPlan) {
112
            $subscription->setSubscriptionPlan($plan);
113
        } elseif ($plan->hasEntityId()) {
114
            $subscriptionPlan = $this->subscriptionPlanRepository->findById($plan->getEntityId());
115
            $subscription->setSubscriptionPlan($subscriptionPlan);
116
        }
117
118
        if ($planPrice instanceof Price) {
119
            $subscription->setPrice($planPrice);
120
        } elseif ($planPrice->hasEntityId()) {
121
            $price = $this->priceRepository->findById($planPrice->getEntityId());
122
            $subscription->setPrice($price);
123
        }
124
        $this->subscriptionRepository->save($subscription);
125
        $this->subscriptionRepository->updateValidUntilForAllActiveSubscriptions($customer, $subscription->getMainExternalReference(), $subscriptionCreationResponse->getBilledUntil());
126
127
        $this->dispatcher->dispatch(new SubscriptionCreated($subscription), SubscriptionCreated::NAME);
128
129
        $obolPaymentDetails = $subscriptionCreationResponse->getPaymentDetails();
130
        if ($obolPaymentDetails) {
131
            $payment = $this->paymentFactory->fromSubscriptionCreation($obolPaymentDetails, $customer);
132
            $payment->addSubscription($subscription);
133
            $this->paymentRepository->save($payment);
134
135
            $this->dispatcher->dispatch(new PaymentCreated($payment, true), PaymentCreated::NAME);
136
        }
137
138
        return $subscription;
139
    }
140
141
    public function startSubscriptionWithDto(CustomerInterface $customer, StartSubscriptionDto $startSubscriptionDto): Subscription
142
    {
143
        if (!$startSubscriptionDto->getPaymentDetailsId()) {
144
            $paymentDetails = $this->paymentDetailsRepository->getDefaultPaymentCardForCustomer($customer);
145
        } else {
146
            $paymentDetails = $this->paymentDetailsRepository->findById($startSubscriptionDto->getPaymentDetailsId());
147
        }
148
149
        $plan = $this->planManager->getPlanByName($startSubscriptionDto->getPlanName());
150
        $planPrice = $plan->getPriceForPaymentSchedule($startSubscriptionDto->getSchedule(), $startSubscriptionDto->getCurrency());
151
152
        return $this->startSubscription($customer, $plan, $planPrice, $paymentDetails, $startSubscriptionDto->getSeatNumbers());
153
    }
154
155
    public function cancelSubscriptionAtEndOfCurrentPeriod(Subscription $subscription): Subscription
156
    {
157
        $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

157
        /** @scrutinizer ignore-call */ 
158
        $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...
158
159
        $cancelRequest = new CancelSubscription();
160
        $cancelRequest->setSubscription($obolSubscription);
161
        $cancelRequest->setInstantCancel(false);
162
163
        $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...
164
165
        $subscription->setStatus(SubscriptionStatus::PENDING_CANCEL);
166
        $subscription->endAtEndOfPeriod();
167
168
        $this->subscriptionRepository->save($subscription);
169
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
170
171
        return $subscription;
172
    }
173
174
    public function cancelSubscriptionInstantly(Subscription $subscription): Subscription
175
    {
176
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
177
178
        $cancelRequest = new CancelSubscription();
179
        $cancelRequest->setSubscription($obolSubscription);
180
        $cancelRequest->setInstantCancel(true);
181
182
        $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...
183
184
        $subscription->setStatus(SubscriptionStatus::CANCELLED);
185
        $subscription->setActive(false);
186
        $subscription->endNow();
187
188
        $this->subscriptionRepository->save($subscription);
189
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
190
191
        return $subscription;
192
    }
193
194
    public function cancelSubscriptionOnDate(Subscription $subscription, \DateTimeInterface $dateTime): Subscription
195
    {
196
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
197
198
        $cancelRequest = new CancelSubscription();
199
        $cancelRequest->setSubscription($obolSubscription);
200
        $cancelRequest->setInstantCancel(false);
201
202
        $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...
203
204
        $subscription->setStatus(SubscriptionStatus::PENDING_CANCEL);
205
        $subscription->setEndedAt($dateTime);
206
        $subscription->setValidUntil($dateTime);
207
208
        $this->subscriptionRepository->save($subscription);
209
        $this->dispatcher->dispatch(new SubscriptionCancelled($subscription), SubscriptionCancelled::NAME);
210
211
        return $subscription;
212
    }
213
214
    public function changeSubscriptionPrice(Subscription $subscription, Price $price, BillingChangeTiming $billingChangeTiming): void
215
    {
216
        $subscription->setPrice($price);
217
        $subscription->setMoneyAmount($price->getAsMoney());
218
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
219
        $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

219
        $obolSubscription->setPriceId(/** @scrutinizer ignore-type */ $price->getExternalReference());
Loading history...
220
        $prorataTye = match ($billingChangeTiming) {
221
            BillingChangeTiming::INSTANTLY => ProrataType::NOW,
222
            default => ProrataType::NONE,
223
        };
224
225
        $this->provider->subscriptions()->updatePrice($obolSubscription, $prorataTye);
226
    }
227
228
    public function changeSubscriptionPlan(Subscription $subscription, SubscriptionPlan $plan, Price $price, BillingChangeTiming $billingChangeTiming): void
229
    {
230
        $subscription->setSubscriptionPlan($plan);
231
        $subscription->setPlanName($plan->getName());
232
        $subscription->setPrice($price);
233
        $subscription->setMoneyAmount($price->getAsMoney());
234
235
        $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription);
236
        $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

236
        $obolSubscription->setPriceId(/** @scrutinizer ignore-type */ $price->getExternalReference());
Loading history...
237
        $prorataTye = match ($billingChangeTiming) {
238
            BillingChangeTiming::INSTANTLY => ProrataType::NOW,
239
            default => ProrataType::NONE,
240
        };
241
242
        $this->provider->subscriptions()->updatePrice($obolSubscription, $prorataTye);
243
    }
244
}
245