This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
![]() |
|||||||
81 | $obolSubscription = $this->subscriptionFactory->createSubscription($billingDetails, $planPrice, $seatNumbers, $hasTrial ?? $plan->getHasTrial(), $trialLengthDays ?? $plan->getTrialLengthDays()); |
||||||
82 | $obolSubscription->setStoredPaymentReference($paymentDetails->getStoredPaymentReference()); |
||||||
0 ignored issues
–
show
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
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. ![]() |
|||||||
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
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
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. ![]() |
|||||||
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
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
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. ![]() |
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
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
![]() |
|||||||
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
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
![]() |
|||||||
241 | $subscription->setPlanName($plan->getName()); |
||||||
242 | $subscription->setPrice($price); |
||||||
0 ignored issues
–
show
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
![]() |
|||||||
243 | $subscription->setMoneyAmount($price->getAsMoney()); |
||||||
244 | |||||||
245 | $obolSubscription = $this->subscriptionFactory->createSubscriptionFromEntity($subscription); |
||||||
246 | $obolSubscription->setPriceId($price->getExternalReference()); |
||||||
0 ignored issues
–
show
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
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. ![]() 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
![]() |
|||||||
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 |