View source  
  <?php
namespace Drupal\commerce_recurring;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Exception\HardDeclineException;
use Drupal\commerce_recurring\Entity\SubscriptionInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
class RecurringOrderManager implements RecurringOrderManagerInterface {
  
  protected $entityTypeManager;
  
  protected $time;
  
  public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
    $this->entityTypeManager = $entity_type_manager;
    $this->time = $time;
  }
  
  public function startTrial(SubscriptionInterface $subscription) {
    $state = $subscription
      ->getState()
      ->getId();
    if ($state != 'trial') {
      throw new \InvalidArgumentException(sprintf('Unexpected subscription state "%s".', $state));
    }
    $billing_schedule = $subscription
      ->getBillingSchedule();
    if (!$billing_schedule
      ->getPlugin()
      ->allowTrials()) {
      throw new \InvalidArgumentException(sprintf('The billing schedule "%s" does not allow trials.', $billing_schedule
        ->id()));
    }
    $start_date = $subscription
      ->getTrialStartDate();
    $end_date = $subscription
      ->getTrialEndDate();
    $trial_period = new BillingPeriod($start_date, $end_date);
    $order = $this
      ->createOrder($subscription, $trial_period);
    $this
      ->applyCharges($order, $subscription, $trial_period);
    
    $subscription
      ->getType()
      ->onSubscriptionTrialStart($subscription, $order);
    $order
      ->save();
    $subscription
      ->addOrder($order);
    $subscription
      ->save();
    return $order;
  }
  
  public function startRecurring(SubscriptionInterface $subscription) {
    $state = $subscription
      ->getState()
      ->getId();
    if ($state != 'active') {
      throw new \InvalidArgumentException(sprintf('Unexpected subscription state "%s".', $state));
    }
    $start_date = $subscription
      ->getStartDate();
    $billing_schedule = $subscription
      ->getBillingSchedule();
    $billing_period = $billing_schedule
      ->getPlugin()
      ->generateFirstBillingPeriod($start_date);
    $subscription
      ->setNextRenewalTime($billing_period
      ->getEndDate()
      ->getTimestamp());
    $order = $this
      ->createOrder($subscription, $billing_period);
    $this
      ->applyCharges($order, $subscription, $billing_period);
    
    $subscription
      ->getType()
      ->onSubscriptionActivate($subscription, $order);
    $order
      ->save();
    $subscription
      ->addOrder($order);
    $subscription
      ->save();
    return $order;
  }
  
  public function refreshOrder(OrderInterface $order) {
    
    $billing_period_item = $order
      ->get('billing_period')
      ->first();
    $billing_period = $billing_period_item
      ->toBillingPeriod();
    $subscriptions = $this
      ->collectSubscriptions($order);
    $payment_method = $this
      ->selectPaymentMethod($subscriptions);
    $billing_profile = $payment_method ? $payment_method
      ->getBillingProfile() : NULL;
    $payment_gateway_id = $payment_method ? $payment_method
      ->getPaymentGatewayId() : NULL;
    $order
      ->set('billing_profile', $billing_profile);
    $order
      ->set('payment_method', $payment_method);
    $order
      ->set('payment_gateway', $payment_gateway_id);
    foreach ($subscriptions as $subscription) {
      $this
        ->applyCharges($order, $subscription, $billing_period);
    }
    $order_items = $order
      ->getItems();
    
    if (!$order_items) {
      $order
        ->set('state', 'canceled');
    }
    
    foreach ($order_items as $order_item) {
      if ($order_item
        ->isNew()) {
        $order_item->order_id->entity = $order;
      }
    }
  }
  
  public function closeOrder(OrderInterface $order) {
    $order_state = $order
      ->getState()
      ->getId();
    if ($order
      ->isPaid()) {
      if (in_array('mark_paid', array_keys($order
        ->getState()
        ->getTransitions()))) {
        $order
          ->getState()
          ->applyTransitionById('mark_paid');
        $order
          ->save();
      }
    }
    if (in_array($order_state, [
      'canceled',
      'completed',
    ]) || $order
      ->isPaid()) {
      return;
    }
    if ($order_state == 'draft') {
      $order
        ->getState()
        ->applyTransitionById('place');
      $order
        ->save();
    }
    $subscriptions = $this
      ->collectSubscriptions($order);
    $payment_method = $this
      ->selectPaymentMethod($subscriptions);
    if (!$payment_method) {
      throw new HardDeclineException('Payment method not found.');
    }
    $payment_gateway = $payment_method
      ->getPaymentGateway();
    if (!$payment_gateway) {
      throw new HardDeclineException('Payment gateway not found');
    }
    
    $payment_gateway_plugin = $payment_gateway
      ->getPlugin();
    if (!$order
      ->isPaid()) {
      $payment_storage = $this->entityTypeManager
        ->getStorage('commerce_payment');
      
      $payment = $payment_storage
        ->create([
        'payment_gateway' => $payment_gateway
          ->id(),
        'payment_method' => $payment_method
          ->id(),
        'order_id' => $order
          ->id(),
        'amount' => $order
          ->getTotalPrice(),
        'state' => 'new',
      ]);
      
      $payment_gateway_plugin
        ->createPayment($payment);
      $order
        ->getState()
        ->applyTransitionById('mark_paid');
      $order
        ->save();
    }
  }
  
  public function renewOrder(OrderInterface $order) {
    $subscriptions = $this
      ->collectSubscriptions($order);
    
    $subscription = reset($subscriptions);
    if (!$subscription || $subscription
      ->getState()
      ->getId() != 'active') {
      
      return NULL;
    }
    $billing_schedule = $subscription
      ->getBillingSchedule();
    $start_date = $subscription
      ->getStartDate();
    
    $billing_period_item = $order
      ->get('billing_period')
      ->first();
    $current_billing_period = $billing_period_item
      ->toBillingPeriod();
    $next_billing_period = $billing_schedule
      ->getPlugin()
      ->generateNextBillingPeriod($start_date, $current_billing_period);
    $next_order = $this
      ->createOrder($subscription, $next_billing_period);
    $this
      ->applyCharges($next_order, $subscription, $next_billing_period);
    
    $subscription
      ->getType()
      ->onSubscriptionRenew($subscription, $order, $next_order);
    $next_order
      ->save();
    
    $subscription
      ->addOrder($next_order);
    $subscription
      ->setRenewedTime($this->time
      ->getCurrentTime());
    $subscription
      ->setNextRenewalTime($next_billing_period
      ->getEndDate()
      ->getTimestamp());
    $subscription
      ->save();
    return $next_order;
  }
  
  public function collectSubscriptions(OrderInterface $order) {
    $subscriptions = [];
    foreach ($order
      ->getItems() as $order_item) {
      if ($order_item
        ->get('subscription')
        ->isEmpty()) {
        
        continue;
      }
      
      $subscription = $order_item
        ->get('subscription')->entity;
      
      if ($subscription) {
        $subscriptions[$subscription
          ->id()] = $subscription;
      }
    }
    return $subscriptions;
  }
  
  protected function createOrder(SubscriptionInterface $subscription, BillingPeriod $billing_period) {
    $payment_method = $subscription
      ->getPaymentMethod();
    
    $order = $this->entityTypeManager
      ->getStorage('commerce_order')
      ->create([
      'type' => 'recurring',
      'store_id' => $subscription
        ->getStoreId(),
      'uid' => $subscription
        ->getCustomerId(),
      'billing_profile' => $payment_method ? $payment_method
        ->getBillingProfile() : NULL,
      'payment_method' => $payment_method,
      'payment_gateway' => $payment_method ? $payment_method
        ->getPaymentGatewayId() : NULL,
      'billing_period' => $billing_period,
      'billing_schedule' => $subscription
        ->getBillingSchedule(),
    ]);
    return $order;
  }
  
  protected function applyCharges(OrderInterface $order, SubscriptionInterface $subscription, BillingPeriod $billing_period) {
    
    $order_item_storage = $this->entityTypeManager
      ->getStorage('commerce_order_item');
    $existing_order_items = [];
    foreach ($order
      ->getItems() as $order_item) {
      if ($order_item
        ->get('subscription')->target_id == $subscription
        ->id()) {
        $existing_order_items[] = $order_item;
      }
    }
    if ($subscription
      ->getState()
      ->getId() == 'trial') {
      $charges = $subscription
        ->getType()
        ->collectTrialCharges($subscription, $billing_period);
    }
    else {
      $charges = $subscription
        ->getType()
        ->collectCharges($subscription, $billing_period);
    }
    $order_items = [];
    foreach ($charges as $charge) {
      $order_item = array_shift($existing_order_items);
      if (!$order_item) {
        
        $order_item = $order_item_storage
          ->create([
          'type' => $this
            ->getOrderItemTypeId($subscription),
          'order_id' => $order
            ->id(),
          'subscription' => $subscription
            ->id(),
        ]);
      }
      
      $order_item
        ->set('purchased_entity', $charge
        ->getPurchasedEntity());
      $order_item
        ->setTitle($charge
        ->getTitle());
      $order_item
        ->setQuantity($charge
        ->getQuantity());
      $order_item
        ->set('billing_period', $charge
        ->getBillingPeriod());
      
      $order_item
        ->setUnitPrice($charge
        ->getUnitPrice());
      if ($charge
        ->needsProration()) {
        $prorater = $subscription
          ->getBillingSchedule()
          ->getProrater();
        $prorated_unit_price = $prorater
          ->prorateOrderItem($order_item, $charge
          ->getBillingPeriod(), $charge
          ->getFullBillingPeriod());
        $order_item
          ->setUnitPrice($prorated_unit_price, TRUE);
      }
      
      if ($order_item
        ->isNew()) {
        $order_item
          ->save();
      }
      $order_items[] = $order_item;
    }
    $order
      ->setItems($order_items);
    
    if ($existing_order_items) {
      $order_item_storage
        ->delete($existing_order_items);
    }
  }
  
  protected function selectPaymentMethod(array $subscriptions) {
    $payment_methods = [];
    foreach ($subscriptions as $subscription) {
      if ($payment_method = $subscription
        ->getPaymentMethod()) {
        $payment_methods[$payment_method
          ->id()] = $payment_method;
      }
    }
    krsort($payment_methods, SORT_NUMERIC);
    $payment_method = reset($payment_methods);
    return $payment_method ?: NULL;
  }
  
  protected function getOrderItemTypeId(SubscriptionInterface $subscription) {
    if ($purchasable_entity_type_id = $subscription
      ->getType()
      ->getPurchasableEntityTypeId()) {
      return 'recurring_' . str_replace('commerce_', '', $purchasable_entity_type_id);
    }
    else {
      return 'recurring_standalone';
    }
  }
}