contacts_subscriptions-1.x-dev/src/EventSubscriber/OrderSubscriber.php

src/EventSubscriber/OrderSubscriber.php
<?php

namespace Drupal\contacts_subscriptions\EventSubscriber;

use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_order\Event\OrderEvent;
use Drupal\commerce_order\Event\OrderEvents;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\contacts_subscriptions\Entity\SubscriptionInterface;
use Drupal\contacts_subscriptions\SubscriptionsHelper;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Contacts Jobs Subscriptions order event subscriber.
 */
class OrderSubscriber implements EventSubscriberInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected TimeInterface $time;

  /**
   * The logger channel.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected LoggerChannelInterface $logger;

  /**
   * The mail service.
   *
   * @var \Drupal\Core\Mail\MailManagerInterface
   */
  protected MailManagerInterface $mailManager;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  private AccountProxyInterface $currentUser;

  /**
   * The subscription helper service.
   *
   * @var \Drupal\contacts_subscriptions\SubscriptionsHelper
   */
  protected SubscriptionsHelper $subscriptionsHelper;

  /**
   * Constructs a new OrderSubscriber object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   The logger channel.
   * @param \Drupal\Core\Mail\MailManagerInterface $mail
   *   The mail service.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   * @param \Drupal\contacts_subscriptions\SubscriptionsHelper $subscriptions_helper
   *   The subscriptions helper service.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    TimeInterface $time,
    LoggerChannelInterface $logger,
    MailManagerInterface $mail,
    AccountProxyInterface $current_user,
    SubscriptionsHelper $subscriptions_helper
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->time = $time;
    $this->logger = $logger;
    $this->mailManager = $mail;
    $this->currentUser = $current_user;
    $this->subscriptionsHelper = $subscriptions_helper;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    // Trigger post place as early as possible. Non transition specific
    // events happen later, so we'll add to the individual transitions.
    return [
      OrderEvents::ORDER_PAID => ['orderPaid'],
      'commerce_order.paid.pre_transition' => [
        ['triggerPrePlace', 1000],
      ],
      'commerce_order.payment_declined.pre_transition' => [
        ['triggerPostPlace', 1000],
      ],
      'commerce_order.payment_error.pre_transition' => [
        ['triggerPostPlace', 1000],
      ],
      'commerce_order.paid.post_transition' => [
        ['triggerPostPlace', 1000],
        ['subscriptionActivate'],
        ['notifyPaid', -200],
      ],
      'commerce_order.payment_declined.post_transition' => [
        ['triggerPostPlace', 1000],
        ['subscriptionPaymentFailed'],
        ['notifyFailed', -200],
      ],
      'commerce_order.payment_error.post_transition' => [
        ['triggerPostPlace', 1000],
        ['subscriptionPaymentFailed', 0],
        ['notifyFailed', -200],
      ],
      'commerce_order.place.post_transition' => [
        ['clearRenewalProduct'],
      ],
    ];
  }

  /**
   * Activate the subscription on successful payment.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   */
  public function subscriptionActivate(WorkflowTransitionEvent $event): void {
    if (!($order = $this->validateOrder($event))) {
      return;
    }

    $subscription = $this->subscriptionsHelper->getSubscriptionFromOrder($order);

    foreach ($order->getItems() as $item) {
      $product_variation = $item->getPurchasedEntity();
      if ($product_variation instanceof ProductVariationInterface && $product_variation->getProduct()->bundle() === 'subscription') {
        $subscription->set('product', $product_variation->getProductId());
      }
    }

    $renewal = $this->getRenewal($order, $subscription);
    $subscription->set('renewal', $renewal->format(DateTimeItemInterface::DATE_STORAGE_FORMAT));

    $subscription->setRevisionLogMessage("Subscription renewed");

    if (!$this->applyTransitionIfAllowed(
      $subscription,
      'activate',
    )) {

      // If the transition has not been applied - for example, where a user has
      // renewed a subscription before the expiry date - we will still need to
      // save the subscription for the new renewal date to be stored.
      $subscription->save();
    }
  }

  /**
   * Update the subscription on payment failures.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   */
  public function subscriptionPaymentFailed(WorkflowTransitionEvent $event): void {
    if (!($order = $this->validateOrder($event))) {
      return;
    }
    $subscription = $this->subscriptionsHelper->getSubscriptionFromOrder($order);
    $subscription->setRevisionLogMessage('Subscription payment failed');
    $this->applyTransitionIfAllowed($subscription, 'payment_failed');
  }

  /**
   * Ensure the order state is updated on payment completion.
   *
   * @param \Drupal\commerce_order\Event\OrderEvent $event
   *   The order event.
   */
  public function orderPaid(OrderEvent $event): void {
    $order = $event->getOrder();
    if ($order->bundle() !== 'contacts_subscription') {
      return;
    }

    // Apply the transition. This event happens in pre save, so we don't need to
    // save the order.
    $state = $order->getState();
    if ($state->isTransitionAllowed('paid')) {
      $state->applyTransitionById('paid');
    }
  }

  /**
   * Trigger the pre place event when it has been skipped.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   * @param string $event_name
   *   The event name.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The dispatcher.
   */
  public function triggerPrePlace(WorkflowTransitionEvent $event, $event_name, EventDispatcherInterface $dispatcher): void {
    $this->triggerPlaceEvent($event, $dispatcher, 'pre_transition');
  }

  /**
   * Trigger the post place event when it has been skipped.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   * @param string $event_name
   *   The event name.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The dispatcher.
   */
  public function triggerPostPlace(WorkflowTransitionEvent $event, $event_name, EventDispatcherInterface $dispatcher): void {
    $this->triggerPlaceEvent($event, $dispatcher, 'post_transition');
  }

  /**
   * Notify the customer when payment fails.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   */
  public function notifyFailed(WorkflowTransitionEvent $event): void {
    if (!($order = $this->validateOrder($event))) {
      return;
    }

    try {
      $this->sendEmail('paymentFailure', $order);
    }
    catch (\Exception $e) {
      $this->logger->warning('Failed to notify order failure for order ' . $order->id());
    }
  }

  /**
   * Notify the customer when payment is successfully taken.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   */
  public function notifyPaid(WorkflowTransitionEvent $event): void {
    if (!($order = $this->validateOrder($event))) {
      return;
    }

    try {
      $this->sendEmail('paymentSuccess', $order);
    }
    catch (\Exception $e) {
      $this->logger->warning('Failed to notify order payment success for order ' . $order->id());
    }
  }

  /**
   * Clears the renewal product field on the subscription.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   Event.
   */
  public function clearRenewalProduct(WorkflowTransitionEvent $event) {
    if (!($order = $this->validateOrder($event))) {
      return;
    }

    if ($subscription = $this->subscriptionsHelper->getSubscriptionFromOrder($order)) {
      $subscription
        ->set('renewal_product', NULL)
        ->save();
    }
  }

  /**
   * Get and validate the order from the event.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The transition event.
   *
   * @return \Drupal\commerce_order\Entity\OrderInterface|null
   *   The order, if valid.
   */
  protected function validateOrder(WorkflowTransitionEvent $event): ?OrderInterface {
    $order = $event->getEntity();
    return $order instanceof OrderInterface && $order->bundle() === 'contacts_subscription' ?
      $order :
      NULL;
  }

  /**
   * Apply a subscription transition, if allowed.
   *
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
   *   The subscription.
   * @param string $transition_id
   *   The transition ID to apply.
   * @param bool $save
   *   Whether to save if the transition is applied.
   *
   * @return bool
   *   Whether the transition was applied.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  protected function applyTransitionIfAllowed(SubscriptionInterface $subscription, string $transition_id, bool $save = TRUE): bool {
    /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state */
    $state = $subscription->get('status')->first();

    $workflow = $state->getWorkflow();
    $transitions = $workflow->getAllowedTransitions($state->value, $subscription);

    if (!isset($transitions[$transition_id])) {
      return FALSE;
    }

    $subscription->setNewRevision();
    $subscription->setRevisionCreationTime($this->time->getCurrentTime());
    $subscription->setRevisionUserId($this->currentUser->id());
    $state->applyTransition($transitions[$transition_id]);

    if ($save) {
      $subscription->save();
    }

    return TRUE;
  }

  /**
   * Trigger the pre/post place event when it has been skipped.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The workflow transition event.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The dispatcher.
   * @param string $phase
   *   The phase of the transition.
   */
  protected function triggerPlaceEvent(WorkflowTransitionEvent $event, EventDispatcherInterface $dispatcher, string $phase): void {
    if (!($this->validateOrder($event))) {
      return;
    }

    // If this is not the place transition, but we have gone from draft to
    // another post-placed state, trigger the post place event.
    if ($event->getTransition()->getId() === 'place' || $event->getField()->getOriginalId() !== 'draft') {
      return;
    }

    $group_id = $event->getField()->getWorkflow()->getGroup();
    $dispatcher->dispatch(
      $group_id . '.place.' . $phase,
      $event
    );
  }

  /**
   * Send a notification email.
   *
   * @param string $key
   *   The email key.
   * @param \Drupal\commerce_order\Entity\OrderInterface $order
   *   The order.
   */
  protected function sendEmail(string $key, OrderInterface $order): void {
    $customer = $order->getCustomer();
    $to = "{$customer->getDisplayName()} <{$order->getEmail()}>";
    $this->mailManager->mail(
      'contacts_subscriptions',
      $key,
      $to,
      $customer->getPreferredLangcode(),
      ['order' => $order],
    );
  }

  /**
   * Get the renewal date, falling back to a sensible default.
   *
   * @param \Drupal\commerce_order\Entity\OrderInterface $order
   *   The order entity.
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface|null $subscription
   *   The subscription entity.
   *
   * @return \Drupal\Core\Datetime\DrupalDateTime
   *   The renewal date.
   */
  protected function getRenewal(OrderInterface $order, ?SubscriptionInterface $subscription): DrupalDateTime {
    // Check for an explicit renewal.
    $renewal = $order->getData('contacts_subscription_renewal');
    if ($renewal) {
      return new DrupalDateTime($renewal);
    }

    // Work out the default renewal date. If the subscription is active, start
    // from there. Otherwise, use today's date.
    $start_date = $subscription->isActive() ?
      $subscription->getRenewalDate(FALSE) :
      DrupalDateTime::createFromTimestamp($this->time->getRequestTime(), 'UTC');

    // Add our default duration of 1 year.
    return (clone $start_date)->add(new \DateInterval('P1Y'));
  }

}

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

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