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')); } }