uawdijnntqw1x1x1
IP : 216.73.216.179
Hostname : toronto-dev2
Kernel : Linux toronto-dev2 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
srv
/
users
/
craft4
/
apps
/
craft4-newsite-space
/
vendor
/
craftcms
/
commerce
/
src
/
services
/
Subscriptions.php
/
/
<?php /** * @link https://craftcms.com/ * @copyright Copyright (c) Pixel & Tonic, Inc. * @license https://craftcms.github.io/license/ */ namespace craft\commerce\services; use Craft; use craft\commerce\base\Plan; use craft\commerce\base\SubscriptionGatewayInterface; use craft\commerce\elements\Subscription; use craft\commerce\errors\SubscriptionException; use craft\commerce\events\CancelSubscriptionEvent; use craft\commerce\events\CreateSubscriptionEvent; use craft\commerce\events\SubscriptionEvent; use craft\commerce\events\SubscriptionPaymentEvent; use craft\commerce\events\SubscriptionSwitchPlansEvent; use craft\commerce\models\subscriptions\CancelSubscriptionForm; use craft\commerce\models\subscriptions\SubscriptionForm; use craft\commerce\models\subscriptions\SubscriptionPayment; use craft\commerce\models\subscriptions\SwitchPlansForm; use craft\commerce\records\Subscription as SubscriptionRecord; use craft\elements\User; use craft\errors\ElementNotFoundException; use craft\events\ConfigEvent; use craft\events\ModelEvent; use craft\helpers\DateTimeHelper; use craft\helpers\ProjectConfig as ProjectConfigHelper; use craft\models\FieldLayout; use DateTime; use Throwable; use yii\base\Component; use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\UserException; /** * Subscriptions service. * * @author Pixel & Tonic, Inc. <support@pixelandtonic.com> * @since 2.0 */ class Subscriptions extends Component { /** * @event SubscriptionEvent The event that is triggered after a subscription has expired. * * ```php * use craft\commerce\events\SubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_AFTER_EXPIRE_SUBSCRIPTION, * function(SubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * * // Make a call to third party service to de-authorize a user * // ... * } * ); * ``` */ public const EVENT_AFTER_EXPIRE_SUBSCRIPTION = 'afterExpireSubscription'; /** * @event CreateSubscriptionEvent The event that is triggered before a subscription is created. * * You may set the `isValid` property to `false` on the event to prevent the user from being subscribed to the plan. * * ```php * use craft\commerce\events\CreateSubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\elements\User; * use craft\commerce\base\Plan; * use craft\commerce\models\subscriptions\SubscriptionForm; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_BEFORE_CREATE_SUBSCRIPTION, * function(CreateSubscriptionEvent $event) { * // @var User $user * $user = $event->user; * // @var Plan $plan * $plan = $event->plan; * // @var SubscriptionForm $params * $params = $event->parameters; * * // Set the trial days based on some business logic * // ... * } * ); * ``` */ public const EVENT_BEFORE_CREATE_SUBSCRIPTION = 'beforeCreateSubscription'; /** * @event SubscriptionEvent The event that is triggered after a subscription is created. * * ```php * use craft\commerce\events\SubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_AFTER_CREATE_SUBSCRIPTION, * function(SubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * * // Call a third party service to authorize a user * // ... * } * ); * ``` */ public const EVENT_AFTER_CREATE_SUBSCRIPTION = 'afterCreateSubscription'; /** * @event SubscriptionEvent TThe event that is triggered before a subscription gets reactivated. * * You may set the `isValid` property to `false` on the event to prevent the subscription from being reactivated. * * ```php * use craft\commerce\events\SubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_BEFORE_REACTIVATE_SUBSCRIPTION, * function(SubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * * // Use business logic to determine whether the user can reactivate * // ... * } * ); * ``` */ public const EVENT_BEFORE_REACTIVATE_SUBSCRIPTION = 'beforeReactivateSubscription'; /** * @event SubscriptionEvent The event that is triggered after a subscription gets reactivated. * * ```php * use craft\commerce\events\SubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_AFTER_REACTIVATE_SUBSCRIPTION, * function(SubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * * // Re-authorize the user with a third-party service * // ... * } * ); * ``` */ public const EVENT_AFTER_REACTIVATE_SUBSCRIPTION = 'afterReactivateSubscription'; /** * @event SubscriptionSwitchPlansEvent The event that is triggered before a subscription is switched to a different plan. * * You may set the `isValid` property to `false` on the event to prevent the switch from happening. * * ```php * use craft\commerce\events\SubscriptionSwitchPlansEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\base\Plan; * use craft\commerce\elements\Subscription; * use craft\commerce\models\subscriptions\SwitchPlansForm; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN, * function(SubscriptionSwitchPlansEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * // @var Plan $oldPlan * $oldPlan = $event->oldPlan; * // @var Plan $newPlan * $newPlan = $event->newPlan; * // @var SwitchPlansForm $params * $params = $event->parameters; * * // Modify the switch parameters based on some business logic * // ... * } * ); * ``` */ public const EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN = 'beforeSwitchSubscriptionPlan'; /** * @event SubscriptionSwitchPlansEvent The event that is triggered after a subscription gets switched to a different plan. * * ```php * use craft\commerce\events\SubscriptionSwitchPlansEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\base\Plan; * use craft\commerce\elements\Subscription; * use craft\commerce\models\subscriptions\SwitchPlansForm; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN, * function(SubscriptionSwitchPlansEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * // @var Plan $oldPlan * $oldPlan = $event->oldPlan; * // @var Plan $newPlan * $newPlan = $event->newPlan; * // @var SwitchPlansForm $params * $params = $event->parameters; * * // Adjust the user’s permissions on a third party service * // ... * } * ); * ``` */ public const EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN = 'afterSwitchSubscriptionPlan'; /** * @event CancelSubscriptionEvent The event that is triggered before a subscription is canceled. * * You may set the `isValid` property to `false` on the event to prevent the subscription from being canceled. * * ```php * use craft\commerce\events\CancelSubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use craft\commerce\models\subscriptions\CancelSubscriptionForm; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_BEFORE_CANCEL_SUBSCRIPTION, * function(CancelSubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * // @var CancelSubscriptionForm $params * $params = $event->parameters; * * // Check whether the user is permitted to cancel the subscription * // ... * } * ); * ``` */ public const EVENT_BEFORE_CANCEL_SUBSCRIPTION = 'beforeCancelSubscription'; /** * @event CancelSubscriptionEvent The event that is triggered after a subscription gets canceled. * * ```php * use craft\commerce\events\CancelSubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use craft\commerce\models\subscriptions\CancelSubscriptionForm; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_AFTER_CANCEL_SUBSCRIPTION, * function(CancelSubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * // @var CancelSubscriptionForm $params * $params = $event->parameters; * * // Refund the user for the remainder of the subscription * // ... * } * ); * ``` */ public const EVENT_AFTER_CANCEL_SUBSCRIPTION = 'afterCancelSubscription'; /** * @event SubscriptionEvent The event that is triggered before a subscription gets updated. Typically this event is fired when subscription data is updated on the gateway. * * ```php * use craft\commerce\events\SubscriptionEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_BEFORE_UPDATE_SUBSCRIPTION, * function(SubscriptionEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * * // ... * } * ); * ``` */ public const EVENT_BEFORE_UPDATE_SUBSCRIPTION = 'beforeUpdateSubscription'; /** * @event SubscriptionPaymentEvent The event that is triggered when a subscription payment is received. * * ```php * use craft\commerce\events\SubscriptionPaymentEvent; * use craft\commerce\services\Subscriptions; * use craft\commerce\elements\Subscription; * use craft\commerce\models\subscriptions\SubscriptionPayment; * use DateTime; * use yii\base\Event; * * Event::on( * Subscriptions::class, * Subscriptions::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT, * function(SubscriptionPaymentEvent $event) { * // @var Subscription $subscription * $subscription = $event->subscription; * // @var SubscriptionPayment $payment * $payment = $event->payment; * // @var DateTime $until * $until = $event->paidUntil; * * // Update loyalty reward data * // ... * } * ); * ``` */ public const EVENT_RECEIVE_SUBSCRIPTION_PAYMENT = 'receiveSubscriptionPayment'; public const CONFIG_FIELDLAYOUT_KEY = 'commerce.subscriptions.fieldLayouts'; /** * Handle field layout change * * @throws Exception */ public function handleChangedFieldLayout(ConfigEvent $event): void { $data = $event->newValue; ProjectConfigHelper::ensureAllFieldsProcessed(); $fieldsService = Craft::$app->getFields(); if (empty($data) || empty(reset($data))) { // Delete the field layout $fieldsService->deleteLayoutsByType(Subscription::class); return; } // Save the field layout $layout = FieldLayout::createFromConfig(reset($data)); $layout->id = $fieldsService->getLayoutByType(Subscription::class)->id; $layout->type = Subscription::class; $layout->uid = key($data); $fieldsService->saveLayout($layout, false); } /** * @deprecated in 3.4.17. Unused fields will be pruned automatically as field layouts are resaved. */ public function pruneDeletedField(): void { } /** * Handle field layout being deleted */ public function handleDeletedFieldLayout(): void { Craft::$app->getFields()->deleteLayoutsByType(Subscription::class); } /** * Prevent deleting a user if they have any subscriptions - active or otherwise. * * @param ModelEvent $event the event. */ public function beforeDeleteUserHandler(ModelEvent $event): void { /** @var User $user */ $user = $event->sender; // If there are any subscriptions, make sure that this is not allowed. if ($this->doesUserHaveSubscriptions($user->id)) { // TODO revise this stop-gap measure when Craft CMS gets a way to hook into the user delete process. throw new UserException(Craft::t('commerce', 'Unable to delete user {user}: the user has a Craft Commerce subscription.', [ 'user' => $user->id, ])); } } /** * Expire a subscription. * * @param Subscription $subscription subscription to expire * @param DateTime|null $dateTime expiry date time * @return bool whether successfully expired subscription * @throws ElementNotFoundException * @throws Exception * @throws Throwable if cannot expire subscription */ public function expireSubscription(Subscription $subscription, DateTime $dateTime = null): bool { $subscription->isExpired = true; $subscription->dateExpired = $dateTime; if (!$subscription->dateExpired) { $subscription->dateExpired = DateTimeHelper::toDateTime('now'); } Craft::$app->getElements()->saveElement($subscription, false); // fire an 'expireSubscription' event if ($this->hasEventHandlers(self::EVENT_AFTER_EXPIRE_SUBSCRIPTION)) { $this->trigger(self::EVENT_AFTER_EXPIRE_SUBSCRIPTION, new SubscriptionEvent([ 'subscription' => $subscription, ])); } return true; } /** * Returns subscription count for a plan. */ public function getSubscriptionCountByPlanId(int $planId): int { return SubscriptionRecord::find()->where(['planId' => $planId])->count(); } /** * Returns subscription count for a plan. * * @deprecated in 4.0. Use [[getSubscriptionCountByPlanId]] instead. */ public function getSubscriptionCountForPlanById(int $planId): int { return $this->getSubscriptionCountByPlanId($planId); } /** * Return true if the user has any subscriptions at all, even expired ones. */ public function doesUserHaveSubscriptions(int $userId): bool { return (bool)SubscriptionRecord::find()->where(['userId' => $userId])->count(); } /** * Return true if the user has any subscriptions at all, even expired ones. * * @deprecated in 4.0. Use [[doesUserHaveSubscriptions]] instead. */ public function doesUserHaveAnySubscriptions(int $userId): bool { return $this->doesUserHaveSubscriptions($userId); } /** * Subscribe a user to a subscription plan. * * @param User $user the user subscribing to a plan * @param Plan $plan the plan the user is being subscribed to * @param SubscriptionForm $parameters array of additional parameters to use * @param array $fieldValues array of content field values to set * @return Subscription the subscription * @throws ElementNotFoundException * @throws Exception * @throws InvalidConfigException if the gateway does not support subscriptions * @throws SubscriptionException if something went wrong during subscription * @throws Throwable */ public function createSubscription(User $user, Plan $plan, SubscriptionForm $parameters, array $fieldValues = []): Subscription { $gateway = $plan->getGateway(); // fire a 'beforeCreateSubscription' event $event = new CreateSubscriptionEvent(compact('user', 'plan', 'parameters')); $this->trigger(self::EVENT_BEFORE_CREATE_SUBSCRIPTION, $event); if (!$event->isValid) { $error = Craft::t('commerce', 'Subscription for {user} to {plan} prevented by a plugin.', [ 'user' => $user->getFriendlyName(), 'plan' => (string)$plan, ]); Craft::error($error, __METHOD__); throw new SubscriptionException(Craft::t('commerce', 'Unable to subscribe at this time.')); } $response = $gateway->subscribe($user, $plan, $parameters); $failedToStart = $response->isInactive(); $subscription = new Subscription(); $subscription->userId = $user->id; $subscription->planId = $plan->id; $subscription->gatewayId = $plan->gatewayId; $subscription->orderId = null; $subscription->reference = $response->getReference(); $subscription->trialDays = $response->getTrialDays(); $subscription->nextPaymentDate = $response->getNextPaymentDate(); $subscription->subscriptionData = $response->getData(); $subscription->isCanceled = false; $subscription->isExpired = false; $subscription->hasStarted = !$failedToStart; $subscription->isSuspended = $failedToStart; if ($failedToStart) { $subscription->dateSuspended = DateTimeHelper::toDateTime('now'); } $subscription->setFieldValues($fieldValues); Craft::$app->getElements()->saveElement($subscription, false); // Fire an 'afterCreateSubscription' event. if ($this->hasEventHandlers(self::EVENT_AFTER_CREATE_SUBSCRIPTION)) { $this->trigger(self::EVENT_AFTER_CREATE_SUBSCRIPTION, new SubscriptionEvent([ 'subscription' => $subscription, ])); } return $subscription; } /** * Reactivate a subscription. * * @throws InvalidConfigException if the gateway does not support subscriptions * @throws Throwable * @throws ElementNotFoundException * @throws Exception */ public function reactivateSubscription(Subscription $subscription): bool { $gateway = $subscription->getGateway(); if (!$gateway instanceof SubscriptionGatewayInterface) { throw new InvalidConfigException('Gateway does not support subscriptions.'); } // fire a 'beforeReactivateSubscription' event $event = new SubscriptionEvent([ 'subscription' => $subscription, ]); $this->trigger(self::EVENT_BEFORE_REACTIVATE_SUBSCRIPTION, $event); if (!$event->isValid) { $error = Craft::t('commerce', 'Could not reactivate “{reference}”.', [ 'reference' => $subscription->reference, ]); Craft::error($error, __METHOD__); return false; } $response = $gateway->reactivateSubscription($subscription); if (!$response->isScheduledForCancellation()) { $subscription->isCanceled = false; $subscription->dateCanceled = null; $subscription->subscriptionData = $response->getData(); Craft::$app->getElements()->saveElement($subscription, false); // Fire a 'afterReactivateSubscription' event. if ($this->hasEventHandlers(self::EVENT_AFTER_REACTIVATE_SUBSCRIPTION)) { $this->trigger(self::EVENT_AFTER_REACTIVATE_SUBSCRIPTION, new SubscriptionEvent([ 'subscription' => $subscription, ])); } return true; } return false; } /** * Switch a subscription to a different subscription plan. * * @param Subscription $subscription the subscription to modify * @param Plan $plan the plan to change the subscription to * @param SwitchPlansForm $parameters additional parameters to use * @throws ElementNotFoundException * @throws Exception * @throws InvalidConfigException * @throws Throwable */ public function switchSubscriptionPlan(Subscription $subscription, Plan $plan, SwitchPlansForm $parameters): bool { $gateway = $subscription->getGateway(); if (!$gateway instanceof SubscriptionGatewayInterface) { throw new InvalidConfigException('Gateway does not support subscriptions.'); } $oldPlan = $subscription->getPlan(); if (!$plan->canSwitchFrom($oldPlan)) { throw new InvalidConfigException('The migration between these plans is not allowed.'); } // fire a 'beforeSwitchSubscriptionPlan' event $event = new SubscriptionSwitchPlansEvent([ 'oldPlan' => $oldPlan, 'subscription' => $subscription, 'newPlan' => $plan, 'parameters' => $parameters, ]); $this->trigger(self::EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN, $event); if (!$event->isValid) { $error = Craft::t('commerce', 'Could not switch “{reference}” to “{plan}”.', [ 'reference' => $subscription->reference, 'plan' => $plan->reference, ]); Craft::error($error, __METHOD__); return false; } $response = $gateway->switchSubscriptionPlan($subscription, $plan, $parameters); $subscription->planId = $plan->id; $subscription->nextPaymentDate = $response->getNextPaymentDate(); $subscription->subscriptionData = $response->getData(); $subscription->isCanceled = false; $subscription->isExpired = false; Craft::$app->getElements()->saveElement($subscription); // fire an 'afterSwitchSubscriptionPlan' event if ($this->hasEventHandlers(self::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN)) { $this->trigger(self::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN, new SubscriptionSwitchPlansEvent([ 'oldPlan' => $oldPlan, 'subscription' => $subscription, 'newPlan' => $plan, 'parameters' => $parameters, ])); } return true; } /** * Cancel a subscription. * * @throws InvalidConfigException if the gateway does not support subscriptions * @throws SubscriptionException if something went wrong when canceling subscription */ public function cancelSubscription(Subscription $subscription, CancelSubscriptionForm $parameters): bool { $gateway = $subscription->getGateway(); if (!$gateway instanceof SubscriptionGatewayInterface) { throw new InvalidConfigException('Gateway does not support subscriptions.'); } // fire a 'beforeCancelSubscription' event $event = new CancelSubscriptionEvent(compact('subscription', 'parameters')); $this->trigger(self::EVENT_BEFORE_CANCEL_SUBSCRIPTION, $event); if (!$event->isValid) { $error = Craft::t('commerce', 'Could not cancel “{reference}”.', [ 'reference' => $subscription->reference, ]); Craft::error($error, __METHOD__); return false; } $response = $gateway->cancelSubscription($subscription, $parameters); if ($response->isCanceled() || $response->isScheduledForCancellation()) { if ($response->isScheduledForCancellation()) { $subscription->isCanceled = true; $subscription->dateCanceled = DateTimeHelper::toDateTime('now'); } if ($response->isCanceled()) { $subscription->isExpired = true; $subscription->isCanceled = true; $subscription->dateCanceled = DateTimeHelper::toDateTime('now'); $subscription->dateExpired = DateTimeHelper::toDateTime('now'); } $subscription->setSubscriptionData($response->getData()); try { Craft::$app->getElements()->saveElement($subscription, false); // fire an 'afterCancelSubscription' event if ($this->hasEventHandlers(self::EVENT_AFTER_CANCEL_SUBSCRIPTION)) { $this->trigger(self::EVENT_AFTER_CANCEL_SUBSCRIPTION, new CancelSubscriptionEvent(compact('subscription', 'parameters'))); } } catch (Throwable $exception) { Craft::warning('Failed to cancel subscription ' . $subscription->reference . ': ' . $exception->getMessage()); throw new SubscriptionException(Craft::t('commerce', 'Unable to cancel subscription at this time.')); } } return true; } /** * Update a subscription. * * @throws Throwable * @throws ElementNotFoundException * @throws Exception */ public function updateSubscription(Subscription $subscription): bool { if ($this->hasEventHandlers(self::EVENT_BEFORE_UPDATE_SUBSCRIPTION)) { $this->trigger(self::EVENT_BEFORE_UPDATE_SUBSCRIPTION, new SubscriptionEvent([ 'subscription' => $subscription, ])); } return Craft::$app->getElements()->saveElement($subscription); } /** * Receive a payment for a subscription * * @throws Throwable * @throws ElementNotFoundException * @throws Exception */ public function receivePayment(Subscription $subscription, SubscriptionPayment $payment, DateTime $paidUntil): bool { if ($this->hasEventHandlers(self::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT)) { $this->trigger(self::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT, new SubscriptionPaymentEvent(compact('subscription', 'payment', 'paidUntil'))); } $subscription->nextPaymentDate = $paidUntil; return Craft::$app->getElements()->saveElement($subscription); } }
/srv/users/craft4/apps/craft4-newsite-space/vendor/craftcms/commerce/src/services/Subscriptions.php