braintree_cashier-8.x-4.x-dev/src/SubscriptionService.php

src/SubscriptionService.php
<?php

namespace Drupal\braintree_cashier;

use Braintree\Plan;
use Braintree\Result\Error;
use Braintree\Subscription;
use Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface;
use Drupal\braintree_cashier\Entity\BraintreeCashierSubscription;
use Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface;
use Drupal\braintree_cashier\Event\BraintreeCashierEvents;
use Drupal\braintree_cashier\Event\BraintreeErrorEvent;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannel;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\Entity\User;
use Drupal\braintree_api\BraintreeApiService;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Formatter\IntlMoneyFormatter;
use Money\Parser\DecimalMoneyParser;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Class SubscriptionService.
 */
class SubscriptionService {

  use StringTranslationTrait;

  /**
   * Drupal\Core\Logger\LoggerChannel definition.
   *
   * @var \Drupal\Core\Logger\LoggerChannel
   */
  protected $logger;

  /**
   * Drupal\braintree_api\BraintreeApiService definition.
   *
   * @var \Drupal\braintree_api\BraintreeApiService
   */
  protected $braintreeApi;

  /**
   * The subscription entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $subscriptionStorage;

  /**
   * The Braintree cashier config.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

  /**
   * The decimal money parser.
   *
   * @var \Money\Parser\DecimalMoneyParser
   */
  protected $moneyParser;

  /**
   * The decimal money formatter.
   *
   * @var \Money\Formatter\DecimalMoneyFormatter
   */
  protected $decimalMoneyFormatter;

  /**
   * The braintree cashier service.
   *
   * @var \Drupal\braintree_cashier\BraintreeCashierService
   */
  protected $bcService;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * The billable user service.
   *
   * @var \Drupal\braintree_cashier\BillableUser
   */
  protected $billableUser;

  /**
   * The currency code.
   *
   * @var string
   */
  protected $currencyCode;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The event dispatcher.
   *
   * @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
   */
  protected $eventDispatcher;

  /**
   * The date formatter.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * The discount entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $discountStorage;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Constructs a new SubscriptionService object.
   *
   * @param \Drupal\Core\Logger\LoggerChannel $logger_channel_braintree_cashier
   *   The braintree_cashier logger channel.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\braintree_api\BraintreeApiService $braintree_api_braintree_api
   *   The Braintree API service.
   * @param \Drupal\braintree_cashier\BraintreeCashierService $bcService
   *   The braintree cashier service.
   * @param \Drupal\Core\Config\ConfigFactory $configFactory
   *   The config factory.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   * @param \Drupal\braintree_cashier\BillableUser $billableUser
   *   The billable user service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler.
   * @param \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $eventDispatcher
   *   The container aware event dispatcher.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
   *   The date formatter service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   */
  public function __construct(LoggerChannel $logger_channel_braintree_cashier, EntityTypeManagerInterface $entity_type_manager, BraintreeApiService $braintree_api_braintree_api, BraintreeCashierService $bcService, ConfigFactory $configFactory, RequestStack $requestStack, BillableUser $billableUser, ModuleHandlerInterface $moduleHandler, ContainerAwareEventDispatcher $eventDispatcher, DateFormatterInterface $dateFormatter, MessengerInterface $messenger) {
    $this->logger = $logger_channel_braintree_cashier;
    $this->subscriptionStorage = $entity_type_manager->getStorage('braintree_cashier_subscription');
    $this->discountStorage = $entity_type_manager->getStorage('braintree_cashier_discount');
    $this->braintreeApi = $braintree_api_braintree_api;
    $this->bcService = $bcService;
    $this->config = $configFactory->get('braintree_cashier.settings');
    $this->requestStack = $requestStack;
    $this->billableUser = $billableUser;
    $this->moduleHandler = $moduleHandler;

    // Setup Money.
    $currencies = new ISOCurrencies();
    $this->moneyParser = new DecimalMoneyParser($currencies);
    $this->decimalMoneyFormatter = new DecimalMoneyFormatter($currencies);

    $this->currencyCode = $this->config->get('currency_code');

    $this->eventDispatcher = $eventDispatcher;
    $this->dateFormatter = $dateFormatter;
    $this->messenger = $messenger;
  }

  /**
   * Cancels the subscription.
   *
   * It will remain active until the end of the currnt period.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function cancel(BraintreeCashierSubscriptionInterface $subscription) {
    if ($this->isBraintreeManaged($subscription)) {
      $braintree_subscription = $this->asBraintreeSubscription($subscription);

      // Cancel now to avoid any more charge attempts on a subscription that is
      // past due.
      if ($braintree_subscription->status === Subscription::PAST_DUE) {
        $this->cancelNow($subscription);
        return;
      }

      if (empty($braintree_subscription->billingPeriodEndDate)) {
        // The billingPeriodEndDate is empty for free trials.
        $this->braintreeApi->getGateway()->subscription()->cancel($braintree_subscription->id);
        $subscription->setPeriodEndDate($braintree_subscription->firstBillingDate->getTimestamp());
      }
      else {
        // Make the current billing cycle the last billing cycle.
        $this->braintreeApi->getGateway()->subscription()->update($braintree_subscription->id, [
          'numberOfBillingCycles' => $braintree_subscription->currentBillingCycle,
        ]);
      }
    }
    $subscription->setCancelAtPeriodEnd(TRUE);
    $subscription->save();
  }

  /**
   * Gets whether the subscription entity is managed by Braintree.
   *
   * A subscription on a free trial that will cancel at period end is not
   * managed by Braintree since the corresponding Braintree subscription will
   * have already been canceled.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @return bool
   *   A boolean indicating whether the subscription is managed by Braintree.
   */
  public function isBraintreeManaged(BraintreeCashierSubscriptionInterface $subscription) {
    return !empty($subscription->getBraintreeSubscriptionId()) && !($subscription->isTrialing() && $subscription->willCancelAtPeriodEnd());
  }

  /**
   * Get the subscription as a Braintree subscription object.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @return \Braintree\Subscription
   *   The braintree subscription object.
   */
  public function asBraintreeSubscription(BraintreeCashierSubscriptionInterface $subscription) {
    return $this->braintreeApi->getGateway()->subscription()->find($subscription->getBraintreeSubscriptionId());
  }

  /**
   * Cancels the subscription immediately.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity to cancel.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function cancelNow(BraintreeCashierSubscriptionInterface $subscription) {
    if ($this->isBraintreeManaged($subscription)) {
      $braintree_subscription = $this->asBraintreeSubscription($subscription);
      $this->braintreeApi->getGateway()->subscription()->cancel($braintree_subscription->id);
    }
    $subscription->setStatus(BraintreeCashierSubscriptionInterface::CANCELED);
    $subscription->save();
  }

  /**
   * Swap a subscription between billing plans.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription_entity
   *   The subscription entity.
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $billing_plan
   *   The billing plan entity to swap to.
   * @param \Drupal\user\Entity\User $user
   *   The user entity.
   *
   * @return \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface
   *   The updated, or new, subscription entity.
   *
   * @throws \Exception
   */
  public function swap(BraintreeCashierSubscriptionInterface $subscription_entity, BraintreeCashierBillingPlanInterface $billing_plan, User $user) {
    $braintree_subscription = $this->asBraintreeSubscription($subscription_entity);
    if ($this->onGracePeriod($subscription_entity) && $braintree_subscription->planId == $billing_plan->getBraintreePlanId()) {
      return $this->resume($subscription_entity);
    }
    if ($this->wouldChangeBillingFrequency($braintree_subscription, $billing_plan)) {
      return $this->swapAcrossFrequencies($braintree_subscription, $billing_plan, $user);
    }

    $new_braintree_plan = $this->bcService->getBraintreeBillingPlan($billing_plan->getBraintreePlanId());

    $result = $this->braintreeApi->getGateway()->subscription()->update($braintree_subscription->id, [
      'planId' => $billing_plan->getBraintreePlanId(),
      'neverExpires' => TRUE,
      'price' => $new_braintree_plan->price,
      'numberOfBillingCycles' => NULL,
      'options' => [
        'prorateCharges' => TRUE,
      ],
    ]);

    if ($result->success) {
      $new_braintree_subscription = $result->subscription;
      return $this->updateSubscriptionEntityBillingPlan($subscription_entity, $billing_plan, $new_braintree_subscription);
    }
    else {
      $event = new BraintreeErrorEvent($user, $result->message, $result);
      $this->eventDispatcher->dispatch(BraintreeCashierEvents::BRAINTREE_ERROR, $event);
    }
  }

  /**
   * Determine if the subscription will cancel at period end but is active.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @return bool
   *   A boolean indicating whether the subscription is on it's grace period.
   */
  public function onGracePeriod(BraintreeCashierSubscriptionInterface $subscription) {
    return $subscription->willCancelAtPeriodEnd() && $subscription->getStatus() == BraintreeCashierSubscriptionInterface::ACTIVE;
  }

  /**
   * Resumes a subscription that on it's grace period.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @return \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface
   *   The subscription entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function resume(BraintreeCashierSubscriptionInterface $subscription) {
    if (!$this->onGracePeriod($subscription)) {
      throw new \LogicException('Unable to resume subscription that is not within grace period.');
    }
    $braintree_subscription = $this->asBraintreeSubscription($subscription);
    $this->braintreeApi->getGateway()->subscription()->update($braintree_subscription->id, [
      'neverExpires' => TRUE,
      'numberOfBillingCycles' => NULL,
    ]);
    $subscription->setCancelAtPeriodEnd(FALSE);
    $subscription->save();

    return $subscription;
  }

  /**
   * Determines if the given plan would alter the billing frequency.
   *
   * @param \Braintree\Subscription $current_subscription
   *   The subscription entity.
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $billing_plan
   *   The billing plan entity.
   *
   * @return bool
   *   A boolean indicating whether the billing frequency would change.
   *
   * @throws \Exception
   */
  protected function wouldChangeBillingFrequency(Subscription $current_subscription, BraintreeCashierBillingPlanInterface $billing_plan) {
    $current_plan = $this->bcService->getBraintreeBillingPlan($current_subscription->planId);
    $target_plan = $this->bcService->getBraintreeBillingPlan($billing_plan->getBraintreePlanId());
    return $current_plan->billingFrequency != $target_plan->billingFrequency;
  }

  /**
   * Swap subscriptions for a user across different billing frequencies.
   *
   * Since Braintree doesn't support this natively, we need to cancel the old
   * subscription and create a new one. We give prorated credit from the old
   * subscription to the new one.
   *
   * @param \Braintree\Subscription $current_braintree_subscription
   *   The old Braintree subscription that will be canceled.
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $billing_plan
   *   The target billing plan for which to create a new subscription.
   * @param \Drupal\user\Entity\User $user
   *   The user for whom the new subscription will be created.
   *
   * @return bool|\Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface|false
   *   The subscription entity, or FALSE on failure.
   *
   * @throws \Braintree\Exception\NotFound
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Exception
   */
  public function swapAcrossFrequencies(Subscription $current_braintree_subscription, BraintreeCashierBillingPlanInterface $billing_plan, User $user) {

    $current_braintree_plan = $this->bcService->getBraintreeBillingPlan($current_braintree_subscription->planId);
    $target_braintree_plan = $this->bcService->getBraintreeBillingPlan($billing_plan->getBraintreePlanId());
    if ($this->switchingMonthlyToYearlyPlan($current_braintree_plan, $target_braintree_plan)) {
      $discount = $this->getDiscountForSwitchToYearlyPlan($current_braintree_subscription);
    }
    else {
      $message = $this->t('Switching between plans with these billing frequencies is not supported. You may only switch between plans with the same billing frequency, or switch from a monthly to a yearly plan. Please try switching to a different plan, or wait until your current plan expires and then purchase another one.');
      $this->messenger->addError($message);
      $this->logger->error($message . ' ' . $this->t('Current Braintree subscription ID: %sid, target Braintree billing plan ID, %pid',
          [
            '%sid' => $current_braintree_subscription->id,
            '%pid' => $target_braintree_plan->id,
          ]));
      return FALSE;
    }

    $options = [];
    if ($discount['amount']->greaterThan($this->moneyParser->parse('0', $this->currencyCode))) {
      $options = [
        'discounts' => [
          'add' => [
            [
              'inheritedFromId' => 'plan-credit',
              'amount' => $this->decimalMoneyFormatter->format($discount['amount']),
              'numberOfBillingCycles' => $discount['number_of_billing_cycles'],
            ],
          ],
        ],
      ];
    }

    // Create a new Braintree subscription.
    $payment_method = $this->billableUser->getPaymentMethod($user);
    $new_braintree_subscription = $this->createBraintreeSubscription($user, $payment_method->token, $billing_plan, $options);

    if (empty($new_braintree_subscription)) {
      return FALSE;
    }
    // Cancel the old subscription entity, whereby we also cancel the old
    // Braintree subscription.
    $old_subscription_entity = $this->findSubscriptionEntity($current_braintree_subscription->id);
    $this->cancelNow($old_subscription_entity);

    // Create a new subscription entity.
    $new_subscription_entity = $this->createSubscriptionEntity($billing_plan, $user, $new_braintree_subscription);
    $new_subscription_entity->save();

    return $new_subscription_entity;
  }

  /**
   * Determines if the user is switching form monthly to yearly billing.
   *
   * @param \Braintree\Plan $current_plan
   *   The current billing plan entity.
   * @param \Braintree\Plan $target_plan
   *   The billing plan entity to change to.
   *
   * @return bool
   *   A boolean indicating if the switch is from monthly to yearly billing.
   */
  protected function switchingMonthlyToYearlyPlan(Plan $current_plan, Plan $target_plan) {
    return $current_plan->billingFrequency == 1 && $target_plan->billingFrequency == 12;
  }

  /**
   * Creates a Braintree Subscription.
   *
   * @param \Drupal\user\Entity\User $user
   *   The user entity.
   * @param string $token
   *   A payment method token.
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $billing_plan
   *   The billing plan entity.
   * @param array $options
   *   An array of subscription options to add to the payload.
   * @param string $coupon
   *   A discount ID.
   *
   * @return bool|\Braintree\Subscription
   *   The braintree subscription entity, or false on failure.
   */
  public function createBraintreeSubscription(User $user, $token, BraintreeCashierBillingPlanInterface $billing_plan, array $options = [], $coupon = NULL) {

    $payload = array_merge([
      'paymentMethodToken' => $token,
      'planId' => $billing_plan->getBraintreePlanId(),
    ], $options);

    if (!empty($coupon)) {
      $payload = $this->addCouponToPayload($coupon, $payload);
    }

    $result = $this->braintreeApi->getGateway()->subscription()->create($payload);

    if (!$result->success) {
      $this->processBraintreeSubscriptionCreateFailure($result);
      $event = new BraintreeErrorEvent($user, $result->message, $result);
      $this->eventDispatcher->dispatch(BraintreeCashierEvents::BRAINTREE_ERROR, $event);
      return FALSE;
    }

    $this->logger->notice('A new Braintree Subscription has been created with Braintree Subscription ID: %id', [
      '%id' => $result->subscription->id,
    ]);

    if ($billing_plan->hasFreeTrial() && !$user->get('had_free_trial')->value) {
      $user->set('had_free_trial', TRUE);
      $user->save();
    }

    return $result->subscription;
  }

  /**
   * Process a failed attempt with Braintree to create a subscription.
   *
   * @param \Braintree\Result\Error $result
   *   The Braintree response object which indicates failure.
   *
   * @see https://developers.braintreepayments.com/reference/general/testing/php#test-amounts
   * to simulate processor error responses.
   */
  private function processBraintreeSubscriptionCreateFailure(Error $result) {

    // Check for validation failures created by the Braintree gateway.
    // @see https://developers.braintreepayments.com/reference/general/result-objects/php#error-results
    if (!empty($result->errors) && empty($result->transaction)) {
      $this->messenger->addError($this->t('This transaction failed with the following error message: %message', [
        '%message' => $result->message,
      ]));
      $admin_message = 'Braintree failed to create the subscription, with the following message: ' . $result->message . '. Technical error details: ';
      foreach ($result->errors->deepAll() as $error) {
        $admin_message .= $error->attribute . ": " . $error->code . " " . $error->message . '. ';
      }
      $this->logger->error($admin_message);
      // There's no need to check for other error types since this transaction
      // attempt would not have reached beyond the Braintree gateway.
      return;
    }

    if (!empty($result->transaction)) {
      $transaction = $result->transaction;
      $this->logger->error('The transaction failed with the following message reported: ' . $result->message);
      switch ($transaction->status) {
        case 'processor_declined':
          $this->bcService->handleProcessorDeclined($transaction->processorResponseCode, $transaction->processorResponseText);
          return;

        case 'processor_settlement_declined':
          $this->bcService->handleProcessorSettlementDeclined($transaction);
          return;

        case 'gateway_rejected':
          $this->bcService->handleGatewayRejected($transaction->gatewayRejectionReason);
          return;
      }
    }

    // The failure is a mystery if this point is reached.
    $this->logger->error('A mysterious transaction failure occurred: ' . $result->message);
    $this->messenger->addError($this->t("It wasn't possible to create a subscription. Our payment processor reported the following error: %error. You have not been charged. Please contact the site administrator.", [
      '%error' => $result->message,
    ]));
  }

  /**
   * Adds the coupon discount to the Braintree payload.
   *
   * @param string $coupon
   *   The coupon ID.
   * @param array $payload
   *   The payload array.
   *
   * @return array
   *   The payload array.
   */
  protected function addCouponToPayload($coupon, array $payload) {
    $this->moduleHandler->alter('braintree_cashier_coupon_code', $coupon);
    if (!isset($payload['discounts']['add'])) {
      $payload['discounts']['add'] = [];
    }

    $payload['discounts']['add'][] = [
      'inheritedFromId' => $coupon,
    ];

    return $payload;
  }

  /**
   * Find the subscription entity for a given braintree subscription id.
   *
   * @param string $braintree_subscription_id
   *   The subscription ID in the Braintree control panel.
   *
   * @return \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface
   *   The corresponding subscription entity.
   *
   * @throws \Exception
   */
  public function findSubscriptionEntity($braintree_subscription_id) {
    $query = $this->subscriptionStorage->getQuery();
    $query->condition('braintree_subscription_id', $braintree_subscription_id);
    $result = $query->execute();
    if (count($result) > 1) {
      $message = $this->t('More than one subscription found for id: %id', ['%id' => $braintree_subscription_id]);
      $this->logger->emergency($message);
      throw new \Exception($message);
    }
    if (empty($result)) {
      $message = $this->t('No subscription found for id: %id', ['%id' => $braintree_subscription_id]);
      $this->logger->emergency($message);
      throw new \Exception($message);
    }
    /** @var \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription */
    $subscription = $this->subscriptionStorage->load(array_shift($result));
    return $subscription;
  }

  /**
   * Creates an active subscription entity.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $billing_plan
   *   The billing plan entity.
   * @param \Drupal\user\Entity\User $user
   *   The user entity.
   * @param \Braintree\Subscription $braintree_subscription
   *   The subscription entity.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the sign up form.
   *
   * @return \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface|false
   *   The subscription entity.
   */
  public function createSubscriptionEntity(BraintreeCashierBillingPlanInterface $billing_plan, User $user, Subscription $braintree_subscription = NULL, FormStateInterface $form_state = NULL) {
    $params = [
      'subscription_type' => $billing_plan->getSubscriptionType(),
      'subscribed_user' => $user->id(),
      'status' => BraintreeCashierSubscriptionInterface::ACTIVE,
      'name' => $billing_plan->getName(),
      'billing_plan' => $billing_plan->id(),
      'roles_to_assign' => $billing_plan->getRolesToAssign(),
      'roles_to_revoke' => $billing_plan->getRolesToRevoke(),
      'is_trialing' => !empty($braintree_subscription->trialPeriod),
    ];
    if (!empty($braintree_subscription->id)) {
      $params['braintree_subscription_id'] = $braintree_subscription->id;
    }

    if (!empty($braintree_subscription->trialPeriod)) {
      // Braintree subscription's do not have a trial start date property.
      $params['trial_start_date'] = time();
    }

    if (!empty($braintree_subscription->discounts)) {
      $discount_braintree_ids = [];
      foreach ($braintree_subscription->discounts as $braintree_discount) {
        $discount_braintree_ids[] = $braintree_discount->id;
      }

      if (!empty($discount_braintree_ids)) {
        $discount_entity_ids = $this->discountStorage->getQuery()
          ->condition('discount_id', $discount_braintree_ids, 'IN')
          ->execute();

        if (!empty($discount_entity_ids)) {
          $params['discount'] = $discount_entity_ids;
        }
      }
    }

    $this->moduleHandler->alter('braintree_cashier_create_subscription_params', $params, $billing_plan, $form_state);

    $subscription_entity = BraintreeCashierSubscription::create($params);

    /** @var \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations */
    $violations = $subscription_entity->validate();
    foreach ($violations as $violation) {
      /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
      $admin_message = $this->t('Constraint validation failed when creating a subscription. Message: %message', [
        '%message' => $violation->getMessage(),
      ]);
      $this->logger->error($admin_message);
    }
    if ($violations->count() > 0) {
      $this->messenger->addError($this->t('An error occurred creating the subscription. Please contact the site administrator.'));
      return FALSE;
    }
    $subscription_entity->save();

    return $subscription_entity;
  }

  /**
   * Updates a subscription entity after swapping.
   *
   * Update the subscription entity when the Braintree subscription with which
   * it's associated is replaced with a Braintree subscription with a new
   * billing plan.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $subscription
   *   The subscription entity that needs updating.
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierBillingPlanInterface $new_billing_plan
   *   The new billing plan used to modify the Braintree subscription.
   * @param \Braintree\Subscription $updated_braintree_subscription
   *   The updated Braintree subscription.
   *
   * @return \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface
   *   The subscription entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function updateSubscriptionEntityBillingPlan(BraintreeCashierSubscriptionInterface $subscription, BraintreeCashierBillingPlanInterface $new_billing_plan, Subscription $updated_braintree_subscription) {
    // Cancel subscription entity in order to invoke hooks on cancellation such
    // as revoking Roles. These hooks shouldn't interact with the Braintree API.
    $subscription->setStatus(BraintreeCashierSubscriptionInterface::CANCELED);
    $subscription->save();

    $subscription->setStatus(BraintreeCashierSubscriptionInterface::ACTIVE)
      ->setCancelAtPeriodEnd(FALSE)
      ->setName($new_billing_plan->getName())
      ->setType($new_billing_plan->getSubscriptionType())
      ->setBillingPlan($new_billing_plan->id())
      ->setRolesToAssign($new_billing_plan->getRolesToAssign())
      ->setRolesToRevoke($new_billing_plan->getRolesToRevoke());

    if (!empty($updated_braintree_subscription->discounts)) {
      $discount_braintree_ids = [];
      foreach ($updated_braintree_subscription->discounts as $braintree_discount) {
        $discount_braintree_ids[] = $braintree_discount->id;
      }

      if (!empty($discount_braintree_ids)) {
        $discount_entity_ids = $this->discountStorage->getQuery()
          ->condition('discount_id', $discount_braintree_ids, 'IN')
          ->execute();

        if (!empty($discount_entity_ids)) {
          $subscription->set('discount', $discount_entity_ids);
        }
      }
    }

    $subscription->save();
    return $subscription;
  }

  /**
   * Gets the money remaining in the current period.
   *
   * @param \Braintree\Subscription $current_subscription
   *   The current Braintree subscription from which to switch.
   *
   * @return \Money\Money
   *   The amount of money remaining in the current period.
   */
  public function moneyRemainingInCurrentPeriod(Subscription $current_subscription) {
    $current_period_start_date = $current_subscription->billingPeriodStartDate->getTimestamp();
    $current_period_end_date = $current_subscription->nextBillingDate->getTimestamp();
    // The multiplier is the fraction of time remaining in the current period.
    $multiplier = ($current_period_end_date - time()) / ($current_period_end_date - $current_period_start_date);
    $amount = $this->moneyParser->parse($current_subscription->price, $this->currencyCode);
    return $amount->multiply($multiplier);
  }

  /**
   * Gets the discount to apply for a switch to a yearly plan.
   *
   * @param \Braintree\Subscription $current_subscription
   *   The current Braintree subscription.
   *
   * @return array
   *   A discount array with keys:
   *   - 'amount': Money object representing the amount of the discount.
   *   - 'number_of_billing_periods': A natural number representing the number
   *     of periods over which to apply the discount.
   */
  public function getDiscountForSwitchToYearlyPlan(Subscription $current_subscription) {
    $amount = $this->moneyRemainingInCurrentPeriod($current_subscription);

    return [
      'amount' => $amount,
      'number_of_billing_cycles' => 1,
    ];
  }

  /**
   * Gets the period end date of the current subscription.
   *
   * @param \Drupal\braintree_cashier\Entity\BraintreeCashierSubscriptionInterface $current_subscription
   *   The current subscription entity.
   *
   * @return string
   *   The 'html_date' formatted period end date.
   */
  public function getFormattedPeriodEndDate(BraintreeCashierSubscriptionInterface $current_subscription) {
    $timestamp = '';
    if ($this->isBraintreeManaged($current_subscription)) {
      // @todo use the asBraintreeSubscription method.
      $braintree_subscription = $this->braintreeApi->getGateway()->subscription()->find($current_subscription->getBraintreeSubscriptionId());
      if (empty($braintree_subscription->billingPeriodEndDate)) {
        // The subscription must be on a free trial.
        $timestamp = $braintree_subscription->nextBillingDate->getTimestamp();
      }
      else {
        $timestamp = $braintree_subscription->billingPeriodEndDate->getTimestamp();
      }
    }
    elseif (!empty($current_subscription->getPeriodEndDate())) {
      $timestamp = $current_subscription->getPeriodEndDate();
    }
    $data = [
      'formatted_period_end_date' => !empty($timestamp) ? $this->dateFormatter->format($timestamp, $this->config->get('date_format')) : '',
      'timestamp' => $timestamp,
      'subscription_entity' => $current_subscription,
    ];
    $this->moduleHandler->alter('braintree_cashier_formatted_period_end_date', $data);
    return $data['formatted_period_end_date'];
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc