contacts_subscriptions-1.x-dev/src/InvoiceManager.php

src/InvoiceManager.php
<?php

namespace Drupal\contacts_subscriptions;

use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Exception\DeclineException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_stripe\Plugin\Commerce\PaymentGateway\StripeInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\contacts_subscriptions\Entity\SubscriptionInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\user\UserInterface;

/**
 * The Invoice Manager service.
 */
class InvoiceManager {

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

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

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

  /**
   * The contacts jobs commerce config.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected ImmutableConfig $config;

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

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

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

  /**
   * Constructs an InvoiceManager 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\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   The logger channel.
   * @param \Drupal\contacts_subscriptions\SubscriptionsHelper $subscriptions_helper
   *   The subscriptions helper.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, DateFormatterInterface $date_formatter, ConfigFactoryInterface $config_factory, AccountProxyInterface $current_user, LoggerChannelInterface $logger, SubscriptionsHelper $subscriptions_helper) {
    $this->entityTypeManager = $entity_type_manager;
    $this->time = $time;
    $this->dateFormatter = $date_formatter;
    $this->config = $config_factory->get('contacts_jobs_commerce.settings');
    $this->currentUser = $current_user;
    $this->logger = $logger;
    $this->subscriptionsHelper = $subscriptions_helper;
  }

  /**
   * Generate the subscription invoice for a user an organisation the status.
   *
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
   *   The subscription entity.
   * @param \Drupal\commerce_product\Entity\ProductVariationInterface|null $variation
   *   Optionally provide a variation to use. If not provided, the default
   *   variation of the product from the profile will be used. If given, we will
   *   set the product on the profile.
   *
   * @return bool
   *   If the payment processing succeeded.
   *
   * @throws \InvalidArgumentException
   *   Thrown if:
   *   - $organisation is not an organisation
   *   - $variation is not given and there is no variation set for the
   *     organisation.
   *   - $variation (given or retrieved) is not a subscription.
   */
  public function generateInvoice(SubscriptionInterface $subscription, ProductVariationInterface $variation = NULL): bool {

    if (!$variation) {
      $variation = $this->subscriptionsHelper->getProductVariation($subscription);
    }
    if ($variation->getProduct()->bundle() !== 'subscription') {
      throw new \InvalidArgumentException("Product variation {$variation->id()} is not a subscription.");
    }

    $payment_needed = $variation->getPrice()->isPositive();

    // If no payment is needed, update subscription and return.
    if (!$payment_needed) {
      $this->renewSubscription($subscription, $variation);
      return TRUE;
    }

    // Create the order.
    $order = $this->createOrder($subscription, $variation);
    $paid = $this->processPayment($order);

    // Don't update the subscription until a payment has succeeded.
    if ($paid) {
      $this->renewSubscription($subscription, $variation);
    }

    // If payment fails, return the variable to allow the code calling this
    // service to decide what to do.
    return $paid;
  }

  /**
   * Renew the subscription for another year.
   *
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
   *   The subscription entity.
   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $variation
   *   The product variation.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function renewSubscription(SubscriptionInterface $subscription, ProductVariationInterface $variation): void {
    $start_date = $subscription->getRenewalDate() ??
      DrupalDateTime::createFromTimestamp($this->time->getRequestTime());
    $next_renewal_date = (clone $start_date)->add(new \DateInterval('P1Y'));

    // Update the subscription renewal and product on the profile. The state
    // will be handled by order state transitions.
    $subscription
      ->set('renewal', $next_renewal_date->format(DateTimeItem::DATE_STORAGE_FORMAT))
      ->set('product', $variation->getProductId());
    $subscription->setNewRevision(TRUE);
    $subscription->setRevisionCreationTime($this->time->getCurrentTime());
    $subscription->setRevisionLogMessage('Subscription renewal generated.');

    $subscription->save();
  }

  /**
   * Get the store ID for the job order.
   *
   * @return int|null
   *   The store ID, if there is one.
   *
   * @todo Abstract
   *   \Drupal\contacts_jobs_commerce\Form\PaymentDetailsForm::getStoreId into
   *   a trait or service that can be commonly used.
   */
  protected function getStoreId(): ?int {
    // Check our config.
    if ($store_id = $this->config->get('store_id')) {
      return $store_id;
    }

    // Get the default.
    /** @var \Drupal\commerce_store\StoreStorageInterface $store_storage */
    $store_storage = $this->entityTypeManager->getStorage('commerce_store');
    if ($store = $store_storage->loadDefault()) {
      return $store->id();
    }

    return NULL;
  }

  /**
   * Create the new order.
   *
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
   *   The user the order is for.
   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $variation
   *   The product variation to add to the order.
   * @param bool $place
   *   Whether to place the order.
   *
   * @return \Drupal\commerce_order\Entity\OrderInterface
   *   The generated order.
   */
  public function createOrder(SubscriptionInterface $subscription, ProductVariationInterface $variation, bool $place = TRUE): OrderInterface {
    $user = $subscription->getOwner();

    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
    $order = $this->entityTypeManager
      ->getStorage('commerce_order')
      ->create([
        'type' => 'contacts_subscription',
        'store_id' => $this->getStoreId(),
        'state' => 'draft',
        'uid' => $user->id(),
        'mail' => $user->getEmail(),
        'subscription' => $subscription->id(),
      ]);

    $order->save();

    /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
    $order_item = $this->entityTypeManager
      ->getStorage('commerce_order_item')
      ->createFromPurchasableEntity($variation);
    $order_item->setQuantity(1);

    // Check for a discount.
    if ($discount = $subscription->getOverriddenPrice()) {
      $order_item->setUnitPrice($discount, TRUE);
    }

    $order_item->save();
    $order->addItem($order_item);

    // Get the organisation's stored payment method.
    if ($method = $this->getPaymentMethod($user)) {
      if ($billing_profile = $method->getBillingProfile()) {
        $order->setBillingProfile($billing_profile);
      }

      $order->set('payment_method', $method);
      $order->set('payment_gateway', $method->getPaymentGateway());
    }

    // If requested, apply the place transition. This will set the place
    // timestamp and order number and trigger relevant emails.
    if ($place) {
      $order->getState()->applyTransitionById('place');
    }

    // Save the order.
    $order->save();

    // Clear the discount if it has expired or has no expiry (one time).
    if ($subscription->hasOverrideExpired() !== FALSE) {
      $subscription
        ->set('price_override', NULL)
        ->set('price_override_date', NULL);
    }

    // Return the order.
    return $order;
  }

  /**
   * Attempt to process payment for the order.
   *
   * @param \Drupal\commerce_order\Entity\OrderInterface $order
   *   The order.
   *
   * @return bool
   *   Whether payment was successful.
   */
  protected function processPayment(OrderInterface $order) {
    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $method */
    $method = $order->get('payment_method')->entity;
    if (!$method) {
      $order->getState()->applyTransitionById('needs_payment');
      $order->save();
      return FALSE;
    }

    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
    $payment_gateway = $order->get('payment_gateway')->entity;
    $payment_gateway_plugin = $payment_gateway->getPlugin();

    if (!($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface)) {
      $order->getState()->applyTransitionById('payment_error');
      $order->save();
      return FALSE;
    }

    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
    $payment = $this->entityTypeManager
      ->getStorage('commerce_payment')
      ->create([
        'state' => 'new',
        'amount' => $order->getBalance(),
        'payment_gateway' => $payment_gateway->id(),
        'order_id' => $order->id(),
      ]);
    $payment->set('payment_method', $order->get('payment_method')->entity);
    try {
      if ($payment_gateway_plugin instanceof StripeInterface) {
        $payment_gateway_plugin->createPaymentIntent($order, TRUE, TRUE);
      }

      // Use the default capture behaviour for the payment method.
      $payment_gateway_plugin->createPayment($payment);
      if ($payment->isCompleted()) {
        $order->getState()->applyTransitionById('paid');
        $order->save();
        return TRUE;
      }
    }
    catch (DeclineException $e) {
      $order->getState()->applyTransitionById('payment_declined');
      $order->save();

      $payment->getState()->applyTransitionById('cs_failed_declined');
      $payment->save();
    }
    catch (\Exception $e) {
      $this->logger->error($e->getMessage());

      $order->getState()->applyTransitionById('payment_error');
      $order->save();

      $payment->getState()->applyTransitionById('cs_failed_error');
      $payment->save();
    }
    return FALSE;
  }

  /**
   * Get the expiry date for a subscription with a failed payment.
   *
   * @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
   *   The subscription entity.
   *
   * @return \Drupal\Core\Datetime\DrupalDateTime|null
   *   The expiry date, or NULl if there is none.
   */
  public function getExpiry(SubscriptionInterface $subscription): ?DrupalDateTime {
    if (!$subscription->isActive() || !$subscription->needsPaymentDetails()) {
      return NULL;
    }

    $result = $this->entityTypeManager
      ->getStorage('commerce_order')
      ->getAggregateQuery()
      ->accessCheck(FALSE)
      ->condition('type', 'contacts_subscription')
      ->condition('state', ['payment_declined', 'payment_error'], 'IN')
      ->condition('subscription', $subscription->id())
      ->aggregate('placed', 'MAX')
      ->execute();
    if (empty($result[0]['placed_max'])) {
      return NULL;
    }

    return DrupalDateTime::createFromTimestamp($result[0]['placed_max'])
      ->add(new \DateInterval('PT72H'));
  }

  /**
   * Get the renewal payment method for a user.
   *
   * @param \Drupal\user\UserInterface $user
   *   The user entity.
   *
   * @return \Drupal\commerce_payment\Entity\PaymentMethodInterface|null
   *   The payment method, if there is one.
   */
  public function getPaymentMethod(UserInterface $user): ?PaymentMethodInterface {
    $method_storage = $this->entityTypeManager->getStorage('commerce_payment_method');
    $query = $method_storage->getQuery();
    $query
      ->condition('uid', $user->id())
      ->condition('reusable', TRUE)
      ->condition($query->orConditionGroup()
        ->condition('expires', $this->time->getRequestTime(), '>')
        ->condition('expires', 0))
      ->sort('is_default', 'DESC')
      ->sort('method_id', 'DESC')
      ->range(0, 1);
    $ids = $query->execute();
    return $ids ? $method_storage->load(reset($ids)) : NULL;
  }

}

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

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