class RecurringOrderManager in Commerce Recurring Framework 8
Provides the default recurring order manager.
Currently assumes that there's a single subscription per recurring order.
Hierarchy
- class \Drupal\commerce_recurring\RecurringOrderManager implements RecurringOrderManagerInterface
Expanded class hierarchy of RecurringOrderManager
1 string reference to 'RecurringOrderManager'
1 service uses RecurringOrderManager
File
- src/
RecurringOrderManager.php, line 16
Namespace
Drupal\commerce_recurringView source
class RecurringOrderManager implements RecurringOrderManagerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The time.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Constructs a new RecurringOrderManager object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
$this->entityTypeManager = $entity_type_manager;
$this->time = $time;
}
/**
* {@inheritdoc}
*/
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);
// Allow the type to modify the subscription and order before they're saved.
$subscription
->getType()
->onSubscriptionTrialStart($subscription, $order);
$order
->save();
$subscription
->addOrder($order);
$subscription
->save();
return $order;
}
/**
* {@inheritdoc}
*/
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);
// Allow the type to modify the subscription and order before they're saved.
$subscription
->getType()
->onSubscriptionActivate($subscription, $order);
$order
->save();
$subscription
->addOrder($order);
$subscription
->save();
return $order;
}
/**
* {@inheritdoc}
*/
public function refreshOrder(OrderInterface $order) {
/** @var \Drupal\commerce_recurring\Plugin\Field\FieldType\BillingPeriodItem $billing_period_item */
$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();
// OrderRefresh skips empty orders, an order without items can't stay open.
if (!$order_items) {
$order
->set('state', 'canceled');
}
// The same workaround that \Drupal\commerce_order\OrderRefresh does.
foreach ($order_items as $order_item) {
if ($order_item
->isNew()) {
$order_item->order_id->entity = $order;
}
}
}
/**
* {@inheritdoc}
*/
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');
}
/** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface $payment_gateway_plugin */
$payment_gateway_plugin = $payment_gateway
->getPlugin();
if (!$order
->isPaid()) {
$payment_storage = $this->entityTypeManager
->getStorage('commerce_payment');
/** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
$payment = $payment_storage
->create([
'payment_gateway' => $payment_gateway
->id(),
'payment_method' => $payment_method
->id(),
'order_id' => $order
->id(),
'amount' => $order
->getTotalPrice(),
'state' => 'new',
]);
// The createPayment() call might throw a decline exception, which is
// supposed to be handled by the caller, to allow for dunning.
$payment_gateway_plugin
->createPayment($payment);
$order
->getState()
->applyTransitionById('mark_paid');
$order
->save();
}
}
/**
* {@inheritdoc}
*/
public function renewOrder(OrderInterface $order) {
$subscriptions = $this
->collectSubscriptions($order);
/** @var \Drupal\commerce_recurring\Entity\SubscriptionInterface $subscription */
$subscription = reset($subscriptions);
if (!$subscription || $subscription
->getState()
->getId() != 'active') {
// The subscription was deleted or deactivated.
return NULL;
}
$billing_schedule = $subscription
->getBillingSchedule();
$start_date = $subscription
->getStartDate();
/** @var \Drupal\commerce_recurring\Plugin\Field\FieldType\BillingPeriodItem $billing_period_item */
$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);
// Allow the subscription type to modify the order before it is saved.
$subscription
->getType()
->onSubscriptionRenew($subscription, $order, $next_order);
$next_order
->save();
// Update the subscription with the new order and renewal timestamp.
$subscription
->addOrder($next_order);
$subscription
->setRenewedTime($this->time
->getCurrentTime());
$subscription
->setNextRenewalTime($next_billing_period
->getEndDate()
->getTimestamp());
$subscription
->save();
return $next_order;
}
/**
* {@inheritdoc}
*/
public function collectSubscriptions(OrderInterface $order) {
$subscriptions = [];
foreach ($order
->getItems() as $order_item) {
if ($order_item
->get('subscription')
->isEmpty()) {
// A recurring order item without a subscription ID is malformed.
continue;
}
/** @var \Drupal\commerce_recurring\Entity\SubscriptionInterface $subscription */
$subscription = $order_item
->get('subscription')->entity;
// Guard against deleted subscription entities.
if ($subscription) {
$subscriptions[$subscription
->id()] = $subscription;
}
}
return $subscriptions;
}
/**
* Creates a recurring order for the given subscription.
*
* @param \Drupal\commerce_recurring\Entity\SubscriptionInterface $subscription
* The subscription.
* @param \Drupal\commerce_recurring\BillingPeriod $billing_period
* The billing period.
*
* @return \Drupal\commerce_order\Entity\OrderInterface
* The created recurring order, unsaved.
*/
protected function createOrder(SubscriptionInterface $subscription, BillingPeriod $billing_period) {
$payment_method = $subscription
->getPaymentMethod();
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$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;
}
/**
* Applies subscription charges to the given recurring order.
*
* Note: The order items are not saved.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The recurring order.
* @param \Drupal\commerce_recurring\Entity\SubscriptionInterface $subscription
* The subscription.
* @param \Drupal\commerce_recurring\BillingPeriod $billing_period
* The billing period.
*/
protected function applyCharges(OrderInterface $order, SubscriptionInterface $subscription, BillingPeriod $billing_period) {
/** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */
$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) {
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $order_item_storage
->create([
'type' => $this
->getOrderItemTypeId($subscription),
'order_id' => $order
->id(),
'subscription' => $subscription
->id(),
]);
}
// @todo Add a purchased_entity setter to OrderItemInterface.
$order_item
->set('purchased_entity', $charge
->getPurchasedEntity());
$order_item
->setTitle($charge
->getTitle());
$order_item
->setQuantity($charge
->getQuantity());
$order_item
->set('billing_period', $charge
->getBillingPeriod());
// Populate the initial unit price, then prorate it.
$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);
}
// Avoid setting unsaved order items for now, to avoid #3017259.
if ($order_item
->isNew()) {
$order_item
->save();
}
$order_items[] = $order_item;
}
$order
->setItems($order_items);
// Delete any previous leftover order items.
if ($existing_order_items) {
$order_item_storage
->delete($existing_order_items);
}
}
/**
* Selects the payment method for the given subscriptions.
*
* It is assumed that even if the billing schedule allows multiple
* subscriptions per recurring order, there will still be a single enforced
* payment method per customer. In case multiple payment methods are found,
* the more recent one will be used.
*
* @param \Drupal\commerce_recurring\Entity\SubscriptionInterface[] $subscriptions
* The subscriptions.
*
* @return \Drupal\commerce_payment\Entity\PaymentMethodInterface|null
* The payment method, or NULL if none were found.
*/
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;
}
/**
* Gets the order item type ID for the given subscription.
*
* @param \Drupal\commerce_recurring\Entity\SubscriptionInterface $subscription
* The subscription.
*
* @return string
* The order item type ID.
*/
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';
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
RecurringOrderManager:: |
protected | property | The entity type manager. | |
RecurringOrderManager:: |
protected | property | The time. | |
RecurringOrderManager:: |
protected | function | Applies subscription charges to the given recurring order. | |
RecurringOrderManager:: |
public | function |
Closes the given recurring order. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
public | function |
Collects all subscriptions that belong to an order. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
protected | function | Creates a recurring order for the given subscription. | |
RecurringOrderManager:: |
protected | function | Gets the order item type ID for the given subscription. | |
RecurringOrderManager:: |
public | function |
Refreshes the given recurring order. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
public | function |
Renews the given recurring order. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
protected | function | Selects the payment method for the given subscriptions. | |
RecurringOrderManager:: |
public | function |
Starts the recurring process for the given subscription. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
public | function |
Starts the trial for the given subscription. Overrides RecurringOrderManagerInterface:: |
|
RecurringOrderManager:: |
public | function | Constructs a new RecurringOrderManager object. |