Skip to content

Instantly share code, notes, and snippets.

@junaidpv
Last active March 7, 2023 14:16
Show Gist options
  • Save junaidpv/e1404dad2bf957b50947e84686aa1d63 to your computer and use it in GitHub Desktop.
Save junaidpv/e1404dad2bf957b50947e84686aa1d63 to your computer and use it in GitHub Desktop.
Patch to add ability to re-purchase a license to extend it. From comment #148 at https://www.drupal.org/project/commerce_license/issues/2943888#comment-14532157 along with minor improvements.
diff --git a/commerce_license.module b/commerce_license.module
index 918daaf..610dd84 100644
--- a/commerce_license.module
+++ b/commerce_license.module
@@ -71,9 +71,24 @@ function commerce_license_commerce_order_item_delete(EntityInterface $entity) {
return;
}
- // Delete the license.
$license = $entity->license->entity;
- $license->delete();
+ if ($license->state->value == 'renewal_in_progress') {
+ // Already active license was chosen to renew.
+ // Need to put it back in active state.
+ // But directing seting active state will be caught by License::preSave() ane extend its expiry time!
+ // So, firt put it into renewal_cancelled state, then to active.
+ $transition = $license->getState()->getWorkflow()->getTransition('cancel_renewal');
+ $license->getState()->applyTransition($transition);
+ $license->save();
+
+ $transition = $license->getState()->getWorkflow()->getTransition('confirm');
+ $license->getState()->applyTransition($transition);
+ $license->save();
+ }
+ else {
+ // Delete the license.
+ $license->delete();
+ }
}
/**
diff --git a/commerce_license.services.yml b/commerce_license.services.yml
index 7269c12..e717ef5 100644
--- a/commerce_license.services.yml
+++ b/commerce_license.services.yml
@@ -24,6 +24,15 @@ services:
tags:
- { name: commerce_order.order_processor }
+ commerce_license.license_renewal_cart_event_subscriber:
+ class: Drupal\commerce_license\EventSubscriber\LicenseRenewalCartEventSubscriber
+ arguments:
+ - '@entity_type.manager'
+ - '@messenger'
+ - '@date.formatter'
+ tags:
+ - { name: event_subscriber }
+
commerce_license.license_multiples_cart_event_subscriber:
class: Drupal\commerce_license\EventSubscriber\LicenseMultiplesCartEventSubscriber
arguments: ['@messenger']
diff --git a/commerce_license.workflows.yml b/commerce_license.workflows.yml
index 1e1ddf4..4a56eb1 100644
--- a/commerce_license.workflows.yml
+++ b/commerce_license.workflows.yml
@@ -11,6 +11,10 @@ license_default:
label: Pending
active:
label: Active
+ renewal_in_progress:
+ label: Renewal in progress
+ renewal_cancelled:
+ label: Renewal cancelled
suspended:
label: Suspended
expired:
@@ -31,8 +35,16 @@ license_default:
to: pending
confirm:
label: 'Confirm Activation'
- from: [new, pending]
+ from: [new, pending, renewal_in_progress, renewal_cancelled]
to: active
+ renewal_in_progress:
+ label: 'Renewal in progress'
+ from: [active]
+ to: renewal_in_progress
+ cancel_renewal:
+ label: 'Cancel renewal'
+ from: [renewal_in_progress]
+ to: renewal_cancelled
suspend:
label: 'Suspend'
from: [active]
diff --git a/composer.json b/composer.json
index 7a85a63..c602b00 100644
--- a/composer.json
+++ b/composer.json
@@ -16,6 +16,7 @@
},
"require-dev": {
"drupal/commerce_recurring": "^1.0@beta",
- "drupal/recurring_period": "^1.0"
+ "drupal/recurring_period": "^1.0",
+ "dms/phpunit-arraysubset-asserts": "^0.3"
}
}
diff --git a/config/schema/commerce_license.schema.yml b/config/schema/commerce_license.schema.yml
index 2b112e7..e1d0446 100644
--- a/config/schema/commerce_license.schema.yml
+++ b/config/schema/commerce_license.schema.yml
@@ -8,6 +8,15 @@ commerce_product.commerce_product_variation_type.*.third_party.commerce_license:
activate_on_place:
type: boolean
label: 'Whether to activate a license when the order is placed'
+ allow_renewal:
+ type: boolean
+ label: 'Allow renewal of license before expiration'
+ interval:
+ type: text
+ label: 'Allow renewal of license within this timeframe of expiration (multiplier)'
+ period:
+ type: text
+ label: 'Allow renewal of license within this timeframe of expiration (period unit)'
views.field.commerce_license__entity_label:
type: views.field.entity_label
diff --git a/src/Entity/License.php b/src/Entity/License.php
index 6470b53..f9039c8 100644
--- a/src/Entity/License.php
+++ b/src/Entity/License.php
@@ -2,6 +2,7 @@
namespace Drupal\commerce_license\Entity;
+use Drupal\commerce_product\Entity\ProductVariationType;
use Drupal\commerce\EntityOwnerTrait;
use Drupal\commerce_license\Plugin\Commerce\LicenseType\LicenseTypeInterface;
use Drupal\commerce_order\Entity\OrderInterface;
@@ -10,6 +11,7 @@ use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines the License entity.
@@ -71,6 +73,16 @@ class License extends ContentEntityBase implements LicenseInterface {
use EntityChangedTrait;
use EntityOwnerTrait;
+ use StringTranslationTrait;
+
+ /**
+ * The renewal window start time.
+ *
+ * Calculated in the case of a renewable license.
+ *
+ * @var int|null
+ */
+ protected $renewalWindowStartTime = NULL;
/**
* {@inheritdoc}
@@ -112,9 +124,22 @@ class License extends ContentEntityBase implements LicenseInterface {
$this->setGrantedTime($activation_time);
}
else {
- // The license has previously been granted, and is therefore being
- // re-activated after a lapse. Set the 'renewed' timestamp.
- $this->setRenewedTime($activation_time);
+ if (isset($this->original) && $this->original->state->value != 'renewal_cancelled') {
+ // The license has previously been granted, and is therefore being
+ // re-activated after a lapse. Set the 'renewed' timestamp.
+ $this->setRenewedTime($activation_time);
+ }
+ }
+
+ // Renewal completed.
+ if (isset($this->original) && $this->original->state->value == 'renewal_in_progress') {
+ $expires_time = $this->getExpiresTime();
+ if ($expires_time < $activation_time) {
+ $expires_time = $activation_time;
+ }
+ $this->setExpiresTime(
+ $this->calculateExpirationTime($expires_time)
+ );
}
// Set the expiry time on a new license, but allow licenses to be
@@ -125,7 +150,7 @@ class License extends ContentEntityBase implements LicenseInterface {
}
// The state is being moved away from 'active'.
- if (isset($this->original) && $this->original->getState()->getId() == 'active') {
+ if (isset($this->original) && $this->original->getState()->getId() == 'active' && $this->getState()->getId() != 'renewal_in_progress') {
// The license is revoked.
$this->getTypePlugin()->revokeLicense($this);
}
@@ -234,6 +259,13 @@ class License extends ContentEntityBase implements LicenseInterface {
return $this;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getRenewalWindowStartTime() {
+ return $this->renewalWindowStartTime;
+ }
+
/**
* Calculate the expiration time for this license from a start time.
*
@@ -244,6 +276,8 @@ class License extends ContentEntityBase implements LicenseInterface {
* The expiry timestamp, or the value
* \Drupal\recurring_period\Plugin\RecurringPeriod\RecurringPeriodInterface::UNLIMITED
* if the license does not expire.
+ *
+ * @throws \Exception
*/
protected function calculateExpirationTime($start) {
/** @var \Drupal\recurring_period\Plugin\RecurringPeriod\RecurringPeriodInterface $expiration_type_plugin */
@@ -472,4 +506,63 @@ class License extends ContentEntityBase implements LicenseInterface {
return $fields;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function canRenew() {
+ if (!in_array($this->state->value, ['active', 'renewal_in_progress'])) {
+ return FALSE;
+ }
+
+ $variation = $this->getPurchasedEntity();
+ $product_variation_type_id = $variation->bundle();
+ $product_variation_type = ProductVariationType::load(
+ $product_variation_type_id
+ );
+
+ $allow_renewal = $product_variation_type->getThirdPartySetting(
+ 'commerce_license',
+ 'allow_renewal',
+ FALSE
+ );
+ if (!$allow_renewal) {
+ return FALSE;
+ }
+
+ $allow_renewal_window_interval = $product_variation_type->getThirdPartySetting(
+ 'commerce_license',
+ 'interval'
+ );
+ $allow_renewal_window_period = $product_variation_type->getThirdPartySetting(
+ 'commerce_license',
+ 'period'
+ );
+
+ // Code from Drupal\recurring_period\Plugin\RecurringPeriod\RollingInterval
+ // method calculateDate.
+ // Create a DateInterval that represents the interval.
+ // TODO: This can be removed when https://www.drupal.org/node/2900435 lands.
+ $interval_plugin_definition = \Drupal::service(
+ 'plugin.manager.interval.intervals'
+ )->getDefinition($allow_renewal_window_period);
+ $value = $allow_renewal_window_interval * $interval_plugin_definition['multiplier'];
+ $date_interval = \DateInterval::createFromDateString(
+ $value . ' ' . $interval_plugin_definition['php']
+ );
+ $renewal_window_start_time = (new \DateTime(
+ date('r', $this->getExpiresTime())
+ ))
+ ->setTimezone(
+ new \DateTimeZone(commerce_license_get_user_timezone($this->getOwner()))
+ );
+ $renewal_window_start_time->sub($date_interval);
+ $this->renewalWindowStartTime = $renewal_window_start_time->getTimestamp();
+ if ($this->renewalWindowStartTime < \Drupal::time()->getRequestTime()) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
}
diff --git a/src/Entity/LicenseInterface.php b/src/Entity/LicenseInterface.php
index cb93347..a3fa57b 100644
--- a/src/Entity/LicenseInterface.php
+++ b/src/Entity/LicenseInterface.php
@@ -89,6 +89,16 @@ interface LicenseInterface extends ContentEntityInterface, EntityChangedInterfac
*/
public function setRenewedTime($timestamp);
+ /**
+ * The renewal window start time.
+ *
+ * Calculated in the case of a renewable license.
+ *
+ * @return int|null
+ * The renewal window start time.
+ */
+ public function getRenewalWindowStartTime();
+
/**
* Get an unconfigured instance of the associated license type plugin.
*
@@ -154,6 +164,14 @@ interface LicenseInterface extends ContentEntityInterface, EntityChangedInterfac
*/
public static function getWorkflowId(LicenseInterface $license);
+ /**
+ * Checks if the license can be renewed at this time.
+ *
+ * @return bool
+ * TRUE if the license can be renewed. FALSE otherwise.
+ */
+ public function canRenew();
+
/**
* Gets the originating order.
*
diff --git a/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php b/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
index 66b07e7..b15167b 100644
--- a/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
+++ b/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
@@ -62,6 +62,8 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
*
* @param \Drupal\commerce_cart\Event\CartEntityAddEvent $event
* The cart event.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
*/
public function onCartEntityAdd(CartEntityAddEvent $event) {
$order_item = $event->getOrderItem();
@@ -93,6 +95,8 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
*
* @param \Drupal\commerce_cart\Event\CartOrderItemUpdateEvent $event
* The cart event.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
*/
public function onCartItemUpdate(CartOrderItemUpdateEvent $event) {
$order_item = $event->getOrderItem();
diff --git a/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
new file mode 100644
index 0000000..ba000b8
--- /dev/null
+++ b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\commerce_license\EventSubscriber;
+
+use Drupal\commerce_license\Plugin\Commerce\LicenseType\ExistingRightsFromConfigurationCheckingInterface;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\commerce_cart\Event\CartEntityAddEvent;
+use Drupal\commerce_cart\Event\CartEvents;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Handles license renewal.
+ *
+ * Set the already existing license in the order item.
+ */
+class LicenseRenewalCartEventSubscriber implements EventSubscriberInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The messenger service.
+ *
+ * @var \Drupal\Core\Messenger\MessengerInterface
+ */
+ protected $messenger;
+
+ /**
+ * The date formatter service.
+ *
+ * @var \Drupal\Core\Datetime\DateFormatterInterface
+ */
+ protected $dateFormatter;
+
+ /**
+ * The license storage.
+ *
+ * @var \Drupal\commerce_license\LicenseStorage
+ */
+ protected $licenseStorage;
+
+ /**
+ * Constructs a new LicenseRenewalCartEventSubscriber.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+ * The messenger service.
+ * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+ * The date formatter service.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ public function __construct(
+ EntityTypeManagerInterface $entity_type_manager,
+ MessengerInterface $messenger,
+ DateFormatterInterface $date_formatter
+ ) {
+ $this->licenseStorage = $entity_type_manager->getStorage('commerce_license');
+ $this->entityTypeManager = $entity_type_manager;
+ $this->messenger = $messenger;
+ $this->dateFormatter = $date_formatter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+
+ $events = [
+ CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', 100],
+ ];
+ return $events;
+ }
+
+ /**
+ * Sets the already existing license in the order item.
+ *
+ * @param \Drupal\commerce_cart\Event\CartEntityAddEvent $event
+ * The cart event.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ * @throws \Drupal\Core\TypedData\Exception\MissingDataException
+ */
+ public function onCartEntityAdd(CartEntityAddEvent $event) {
+ $order_item = $event->getOrderItem();
+ // Only act if the order item has a license reference field.
+ if (!$order_item->hasField('license')) {
+ return;
+ }
+ // We can't renew license types that don't allow us to find a license
+ // given only a product variation and a user.
+ $variation = $order_item->getPurchasedEntity();
+
+ $license_type_plugin = $variation->get('license_type')->first()->getTargetInstance();
+ if (!($license_type_plugin instanceof ExistingRightsFromConfigurationCheckingInterface)) {
+ return;
+ }
+ $existing_license = $this->licenseStorage->getExistingLicense($variation, $order_item->getOrder()->getCustomerId());
+ if ($existing_license && $existing_license->canRenew()) {
+ $order_item->set('license', $existing_license->id());
+ $order_item->save();
+
+ if ($existing_license->getState()->getId() != 'renewal_in_progress') {
+ $transition = $existing_license->getState()->getWorkflow()->getTransition('renewal_in_progress');
+ $existing_license->getState()->applyTransition($transition);
+ $existing_license->save();
+ }
+
+ // Shows a message with existing and extended dates when order completed.
+ $expiresTime = $existing_license->getExpiresTime();
+ $datetime = (new \DateTimeImmutable())->setTimestamp($expiresTime);
+ $extendedDatetime = $existing_license->getExpirationPlugin()->calculateEnd($datetime);
+
+ // TODO: link here once there is user admin UI for licenses!
+ \Drupal::messenger()->addStatus(
+ t('You have an existing license for @product-label until @expires-time.
+ This will be extended until @extended-date when you complete this order.', [
+ "@product-label" => $existing_license->label(),
+ "@expires-time" => \Drupal::service('date.formatter')->format($expiresTime),
+ "@extended-date" => \Drupal::service('date.formatter')->format($extendedDatetime->getTimestamp()),
+ ])
+ );
+ }
+ elseif ($existing_license) {
+
+ // This will never be fired when expected,
+ // since the CART_ENTITY_ADD is not fired at this point ?
+ $renewal_window_start_time = $existing_license->getRenewalWindowStartTime();
+
+ if (!is_null($renewal_window_start_time)) {
+ $this->messenger->addStatus($this->t('You have an existing license for this product. You will be able to renew your license after %date.', [
+ '%date' => $this->dateFormatter->format($renewal_window_start_time),
+ ]));
+ }
+ }
+ }
+
+}
diff --git a/src/FormAlter/GrantedEntityFormAlter.php b/src/FormAlter/GrantedEntityFormAlter.php
index d54401f..acf037d 100644
--- a/src/FormAlter/GrantedEntityFormAlter.php
+++ b/src/FormAlter/GrantedEntityFormAlter.php
@@ -82,6 +82,12 @@ class GrantedEntityFormAlter {
'uid' => $user_id,
'state' => 'active',
]);
+ if (empty($licenses)) {
+ $licenses = \Drupal::service('entity_type.manager')->getStorage('commerce_license')->loadByProperties([
+ 'uid' => $user_id,
+ 'state' => 'renewal_in_progress',
+ ]);
+ }
// Let each suitable license's plugin alter the form for the license.
foreach ($licenses as $license) {
diff --git a/src/FormAlter/ProductVariationTypeFormAlter.php b/src/FormAlter/ProductVariationTypeFormAlter.php
index fa7ac7b..cfd51d3 100644
--- a/src/FormAlter/ProductVariationTypeFormAlter.php
+++ b/src/FormAlter/ProductVariationTypeFormAlter.php
@@ -80,6 +80,37 @@ class ProductVariationTypeFormAlter implements FormAlterInterface {
'#default_value' => $product_variation_type->getThirdPartySetting('commerce_license', 'activate_on_place', FALSE),
];
+ $our_form['license']['allow_renewal'] = [
+ '#type' => 'checkbox',
+ '#title' => t("Allow renewal before expiration"),
+ '#description' => t(
+ "Allows a customer to renew their license by re-purchasing the product for it."
+ ),
+ '#default_value' => $product_variation_type->getThirdPartySetting('commerce_license', 'allow_renewal', FALSE),
+ ];
+
+ $our_form['license']['allow_renewal_window'] = [
+ '#type' => 'details',
+ '#title' => t("Allow renewal window"),
+ '#open' => TRUE,
+ '#states' => [
+ 'visible' => [
+ 'input#edit-allow-renewal' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $our_form['license']['allow_renewal_window']['interval'] = [
+ '#type' => 'interval',
+ '#title' => t("Allow renewal window"),
+ '#description' => t(
+ "The interval before the license's expiry during which re-purchase is allowed. Prior to this interval, re-purchase is blocked, as normal."
+ ),
+ '#default_value' => [
+ 'interval' => $product_variation_type->getThirdPartySetting('commerce_license', 'interval'),
+ 'period' => $product_variation_type->getThirdPartySetting('commerce_license', 'period'),
+ ],
+ ];
// Insert our form elements into the form after the 'traits' element.
// The form elements don't have their weight set, so we can't use that.
$traits_element_form_array_index = array_search('traits', array_keys($form));
@@ -102,6 +133,13 @@ class ProductVariationTypeFormAlter implements FormAlterInterface {
* Form validation callback.
*
* Ensures that everything joins up when a license trait is used.
+ *
+ * @param $form
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ * @throws \Drupal\Core\Entity\EntityMalformedException
*/
public static function formValidate($form, FormStateInterface $form_state) {
$traits = $form_state->getValue('traits');
@@ -162,6 +200,10 @@ class ProductVariationTypeFormAlter implements FormAlterInterface {
$save_variation_type = TRUE;
$variation_type->unsetThirdPartySetting('commerce_license', 'license_types');
$variation_type->unsetThirdPartySetting('commerce_license', 'activate_on_place');
+ // Do the following 3 require unsetting too?
+ $variation_type->unsetThirdPartySetting('commerce_license', 'allow_renewal');
+ $variation_type->unsetThirdPartySetting('commerce_license', 'interval');
+ $variation_type->unsetThirdPartySetting('commerce_license', 'period');
}
}
else {
@@ -172,6 +214,15 @@ class ProductVariationTypeFormAlter implements FormAlterInterface {
$activate_on_place = $form_state->getValue('activate_on_place');
$variation_type->setThirdPartySetting('commerce_license', 'activate_on_place', $activate_on_place);
+ $allow_renewal = $form_state->getValue('allow_renewal');
+ $variation_type->setThirdPartySetting('commerce_license', 'allow_renewal', $allow_renewal);
+
+ $interval = $form_state->getValue('interval');
+ $variation_type->setThirdPartySetting('commerce_license', 'interval', $interval);
+
+ $period = $form_state->getValue('period');
+ $variation_type->setThirdPartySetting('commerce_license', 'period', $period);
+
$order_item_type_id = $form_state->getValue('orderItemType');
/** @var \Drupal\commerce_order\Entity\OrderItemType $order_item_type */
diff --git a/src/LicenseAvailabilityCheckerExistingRights.php b/src/LicenseAvailabilityCheckerExistingRights.php
index 5f7b6e2..8568cca 100644
--- a/src/LicenseAvailabilityCheckerExistingRights.php
+++ b/src/LicenseAvailabilityCheckerExistingRights.php
@@ -94,10 +94,76 @@ class LicenseAvailabilityCheckerExistingRights implements AvailabilityCheckerInt
// grant.
$customer = $context->getCustomer();
$purchased_entity = $order_item->getPurchasedEntity();
- $license_type_plugin = $purchased_entity->license_type->first()->getTargetInstance();
// Load the full user entity for the plugin.
$user = $this->entityTypeManager->getStorage('user')->load($customer->id());
+
+ // Handle licence renewal.
+ /** @var \Drupal\commerce_license\Entity\LicenseInterface $existing_license */
+ $existing_license = $this->entityTypeManager
+ ->getStorage('commerce_license')
+ ->getExistingLicense($purchased_entity, $user->id());
+
+ if ($existing_license && $existing_license->canRenew()) {
+ return;
+ }
+
+ // Shows a message to indicate window start time,
+ // in case license is renewable but we're out of its renewable window.
+ $unsetNotPurchasableMessage = FALSE;
+ if ($existing_license && !is_null($existing_license->getRenewalWindowStartTime())) {
+ $this->setRenewalStartTimeMessage(
+ $existing_license->getRenewalWindowStartTime(),
+ $purchased_entity->label()
+ );
+
+ // Removes the notPurchasable message.
+ $unsetNotPurchasableMessage = TRUE;
+ // TODO remove Drupal\commerce_order\Plugin\Validation\Constraint message:
+ // @product-label is not available with a quantity of @quantity.
+ }
+
+ return $this->checkPurchasable($purchased_entity, $user, $unsetNotPurchasableMessage);
+ }
+
+ /**
+ * Adds a renewalStartTimeMessage status message to queue.
+ *
+ * @param int|null $renewal_window_start_time
+ * The renewal window start time.
+ * @param string $label
+ * The purchased product label.
+ */
+ private function setRenewalStartTimeMessage($renewal_window_start_time, $label) {
+ \Drupal::messenger()->addStatus(
+ t('You have an existing license for this @product-label. You will be able to renew your license after @date.', [
+ '@date' => \Drupal::service('date.formatter')->format($renewal_window_start_time),
+ '@product-label' => $label,
+ ])
+ );
+ }
+
+ /**
+ * Checks if new license is eligible for purchase.
+ *
+ * Hand over to the license type plugin configured in the product variation,
+ * to let it determine whether the user already has what the license would
+ * grant. Adds a notPurchasableMessage status message to queue.
+ *
+ * @param PurchasableEntityInterface $entity
+ * The purchased entity.
+ * @param \Drupal\Core\Entity\EntityInterface $user
+ * The user the license would be granted to.
+ * @param bool $unsetNotPurchasableMessage
+ * Whether to display a notPurchasableMessage message or not.
+ *
+ * @return mixed
+ * The availability of an order item.
+ *
+ * @throws \Drupal\Core\TypedData\Exception\MissingDataException
+ */
+ private function checkPurchasable($entity, $user, $unsetNotPurchasableMessage) {
+ $license_type_plugin = $entity->get('license_type')->first()->getTargetInstance();
$existing_rights_result = $license_type_plugin->checkUserHasExistingRights($user);
if (!$existing_rights_result->hasExistingRights()) {
@@ -112,7 +178,7 @@ class LicenseAvailabilityCheckerExistingRights implements AvailabilityCheckerInt
$rights_check_message = $existing_rights_result->getOtherUserMessage();
}
$message = $rights_check_message . ' ' . t("You may not purchase the @product-label product.", [
- '@product-label' => $purchased_entity->label(),
+ '@product-label' => $entity->label(),
]);
return AvailabilityResult::unavailable($message);
diff --git a/src/LicenseOrderProcessorMultiples.php b/src/LicenseOrderProcessorMultiples.php
index 78d0a1c..a4c4f16 100644
--- a/src/LicenseOrderProcessorMultiples.php
+++ b/src/LicenseOrderProcessorMultiples.php
@@ -39,32 +39,56 @@ class LicenseOrderProcessorMultiples implements OrderProcessorInterface {
* {@inheritdoc}
*/
public function process(OrderInterface $order) {
+ // Collect licenses by types and configurations. Granting the same license
+ // type with the same configuration should be avoided.
+ /** @var \Drupal\commerce_product\Entity\ProductVariationInterface[] */
+ $purchased_entities_by_license_hash = [];
+
foreach ($order->getItems() as $order_item) {
// Skip order items that do not have a license reference field.
if (!$order_item->hasField('license')) {
continue;
}
- // @todo allow license type plugins to respond here, as for types that
- // collect user data in the checkout form, the same product variation can
- // result in different licenses.
- $quantity = $order_item->getQuantity();
- if ($quantity > 1) {
- // Force the quantity back to 1.
- $order_item->setQuantity(1);
+ /** @var \Drupal\commerce_product\Entity\ProductVariationInterface */
+ $purchased_entity = $order_item->getPurchasedEntity();
+
+ if ($purchased_entity->hasField('license_type') && !$purchased_entity->get('license_type')->isEmpty()) {
+ // Force the quantity to 1.
+ if ($order_item->getQuantity() > 1) {
+ $order_item->setQuantity(1);
+ $this->messenger()->addWarning($this->t('You may only have a single %product-label in your cart.', [
+ '%product-label' => $purchased_entity->label(),
+ ]));
+ }
+
+ /** @var \Drupal\commerce\Plugin\Field\FieldType\PluginItem */
+ $license_type = $purchased_entity->get('license_type')->first();
+ $license_hash = \hash('sha256', \serialize($license_type->getValue()));
- $purchased_entity = $order_item->getPurchasedEntity();
- if ($purchased_entity) {
- // Note that this message shows both when attempting to increase the
- // quantity of a license product already in the cart, and when
- // attempting to put more than 1 of a license product into the cart.
- // In the latter case, the message isn't as clear as it could be, but
- // site builders should be hiding the quantity field from the add to
- // cart form for license products, so this is moot.
- $this->messenger()->addError($this->t("You may only have one of @product-label in your cart.", [
- '@product-label' => $purchased_entity->label(),
+ // Check if this $purchased_entity is already in the cart,
+ if (\in_array($purchased_entity, $purchased_entities_by_license_hash)) {
+ $order->removeItem($order_item);
+ // Remove success message from user facing messages.
+ $this->messenger()->deleteByType($this->messenger()::TYPE_STATUS);
+ $this->messenger()->addError($this->t('You may only have one of %product-label in your cart.', [
+ '%product-label' => $purchased_entity->label(),
+ ]));
+ }
+ // or if another $order_item resolves to the same license,
+ elseif (isset($purchased_entities_by_license_hash[$license_hash])) {
+ $order->removeItem($order_item);
+ // Remove success message from user facing messages.
+ $this->messenger()->deleteByType($this->messenger()::TYPE_STATUS);
+ $this->messenger()->addError($this->t('Removed %removed-product-label as %product-label in your cart already grants the same license.', [
+ '%product-label' => $purchased_entities_by_license_hash[$license_hash]->label(),
+ '%removed-product-label' => $purchased_entity->label(),
]));
}
+ // or add this to the array to check against.
+ else {
+ $purchased_entities_by_license_hash[$license_hash] = $purchased_entity;
+ }
}
}
}
diff --git a/src/LicenseStorage.php b/src/LicenseStorage.php
index fea0a7f..94d45aa 100644
--- a/src/LicenseStorage.php
+++ b/src/LicenseStorage.php
@@ -56,4 +56,23 @@ class LicenseStorage extends CommerceContentEntityStorage implements LicenseStor
return $license;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLicense(ProductVariationInterface $variation, $uid) {
+ $existing_licenses_ids = $this->getQuery()
+ ->condition('state', ['active', 'renewal_in_progress'], 'IN')
+ ->condition('uid', $uid)
+ ->condition('product_variation', $variation->id())
+ ->execute();
+
+ if (!empty($existing_licenses_ids)) {
+ $existing_license_id = array_shift($existing_licenses_ids);
+ return $this->load($existing_license_id);
+ }
+ else {
+ return FALSE;
+ }
+ }
+
}
diff --git a/src/LicenseStorageInterface.php b/src/LicenseStorageInterface.php
index 463f3b3..75ceb2f 100644
--- a/src/LicenseStorageInterface.php
+++ b/src/LicenseStorageInterface.php
@@ -16,6 +16,19 @@ use Drupal\commerce_product\Entity\ProductVariationInterface;
*/
interface LicenseStorageInterface extends ContentEntityStorageInterface {
+ /**
+ * Get existing active license given a product variation and a user ID.
+ *
+ * @param \Drupal\commerce_product\Entity\ProductVariationInterface $variation
+ * The product variation.
+ * @param int $uid
+ * The uid for whom the license will be retrieved.
+ *
+ * @return \Drupal\commerce_license\Entity\LicenseInterface|false
+ * An existing license entity. FALSE otherwise.
+ */
+ public function getExistingLicense(ProductVariationInterface $variation, $uid);
+
/**
* Creates a new license from an order item.
*
diff --git a/src/Plugin/AdvancedQueue/JobType/LicenseExpire.php b/src/Plugin/AdvancedQueue/JobType/LicenseExpire.php
index da51e80..e8f8de6 100644
--- a/src/Plugin/AdvancedQueue/JobType/LicenseExpire.php
+++ b/src/Plugin/AdvancedQueue/JobType/LicenseExpire.php
@@ -8,6 +8,7 @@ use Drupal\advancedqueue\Plugin\AdvancedQueue\JobType\JobTypeBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Component\Datetime\TimeInterface;
/**
* Provides the job type for expiring licenses.
@@ -26,6 +27,13 @@ class LicenseExpire extends JobTypeBase implements ContainerFactoryPluginInterfa
*/
protected $entityTypeManager;
+ /**
+ * The time.
+ *
+ * @var \Drupal\Component\Datetime\TimeInterface
+ */
+ protected $time;
+
/**
* Constructs a new LicenseExpire object.
*
@@ -37,11 +45,14 @@ class LicenseExpire extends JobTypeBase implements ContainerFactoryPluginInterfa
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
+ * @param \Drupal\Component\Datetime\TimeInterface $time
+ * The time.
*/
- public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
+ $this->time = $time;
}
/**
@@ -52,7 +63,8 @@ class LicenseExpire extends JobTypeBase implements ContainerFactoryPluginInterfa
$configuration,
$plugin_id,
$plugin_definition,
- $container->get('entity_type.manager')
+ $container->get('entity_type.manager'),
+ $container->get('datetime.time')
);
}
@@ -72,6 +84,10 @@ class LicenseExpire extends JobTypeBase implements ContainerFactoryPluginInterfa
return JobResult::failure('License is no longer active.');
}
+ if ($license->getExpiresTime() > $this->time->getRequestTime()) {
+ return JobResult::failure('License is not expired.');
+ }
+
try {
// Set the license to expired. The plugin will take care of revoking it.
$license->state = 'expired';
diff --git a/tests/modules/commerce_license_test/src/Plugin/Commerce/LicenseType/RenewableLicenseType.php b/tests/modules/commerce_license_test/src/Plugin/Commerce/LicenseType/RenewableLicenseType.php
new file mode 100644
index 0000000..7a0608b
--- /dev/null
+++ b/tests/modules/commerce_license_test/src/Plugin/Commerce/LicenseType/RenewableLicenseType.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\commerce_license_test\Plugin\Commerce\LicenseType;
+
+use Drupal\commerce_license\ExistingRights\ExistingRightsResult;
+use Drupal\commerce_license\Plugin\Commerce\LicenseType\ExistingRightsFromConfigurationCheckingInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * This license type plugin used for renewable case.
+ *
+ * @CommerceLicenseType(
+ * id = "renewable",
+ * label = @Translation("Renewable license"),
+ * )
+ */
+class RenewableLicenseType extends TestLicenseBase implements ExistingRightsFromConfigurationCheckingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkUserHasExistingRights(UserInterface $user) {
+ return ExistingRightsResult::rightsExistIf(
+ TRUE,
+ $this->t("You already have the rights."),
+ $this->t("The user already has the rights.")
+ );
+ }
+
+}
diff --git a/tests/src/Kernel/CommerceAvailabilityExistingRightsTest.php b/tests/src/Kernel/CommerceAvailabilityExistingRightsTest.php
index 75858a8..444409a 100644
--- a/tests/src/Kernel/CommerceAvailabilityExistingRightsTest.php
+++ b/tests/src/Kernel/CommerceAvailabilityExistingRightsTest.php
@@ -50,6 +50,8 @@ class CommerceAvailabilityExistingRightsTest extends CartKernelTestBase {
protected function setUp(): void {
parent::setUp();
+ $this->installEntitySchema('commerce_license');
+
$this->createUser();
// Create an order type for licenses which uses the fulfillment workflow.
diff --git a/tests/src/Kernel/CommerceOrderSyncRenewalTest.php b/tests/src/Kernel/CommerceOrderSyncRenewalTest.php
new file mode 100644
index 0000000..5e8403a
--- /dev/null
+++ b/tests/src/Kernel/CommerceOrderSyncRenewalTest.php
@@ -0,0 +1,447 @@
+<?php
+
+namespace Drupal\Tests\commerce_license\Kernel;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
+use Drupal\Tests\commerce_cart\Traits\CartManagerTestTrait;
+
+/**
+ * Tests renewable behaviour on the license.
+ *
+ * @group commerce_license
+ */
+class CommerceOrderSyncRenewalTest extends CommerceKernelTestBase {
+
+ use CartManagerTestTrait;
+ use LicenseOrderCompletionTestTrait;
+
+ /**
+ * The order type.
+ *
+ * @var \Drupal\commerce_order\Entity\OrderType
+ */
+ protected $orderType;
+
+ /**
+ * The product variation type.
+ *
+ * @var \Drupal\commerce_product\Entity\ProductVariationType
+ */
+ protected $variationType;
+
+ /**
+ * The product variation type for a non renewable license.
+ *
+ * @var \Drupal\commerce_product\Entity\ProductVariationType
+ */
+ protected $nonRenewableVariationType;
+
+ /**
+ * The variation to test against.
+ *
+ * @var \Drupal\commerce_product\Entity\ProductVariation
+ */
+ protected $variation;
+
+ /**
+ * The non renewable variation to test against.
+ *
+ * @var \Drupal\commerce_product\Entity\ProductVariation
+ */
+ protected $nonRenewableVariation;
+
+ /**
+ * The cart manager.
+ *
+ * @var \Drupal\commerce_cart\CartManagerInterface
+ */
+ protected $cartManager;
+
+ /**
+ * The license storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $licenseStorage;
+
+ /**
+ * The customer.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected $user;
+
+ /**
+ * The license used to test renewable behavior.
+ *
+ * @var \Drupal\commerce_license\Entity\LicenseInterface
+ */
+ protected $license;
+
+ /**
+ * The license used to test non renewable behavior.
+ *
+ * @var \Drupal\commerce_license\Entity\LicenseInterface
+ */
+ protected $nonRenewableLicense;
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = [
+ 'entity_reference_revisions',
+ 'interval',
+ 'path',
+ 'profile',
+ 'state_machine',
+ 'system',
+ 'commerce_product',
+ 'commerce_order',
+ 'recurring_period',
+ 'commerce_license',
+ 'commerce_license_test',
+ 'commerce_number_pattern',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->installEntitySchema('profile');
+ $this->installEntitySchema('commerce_product');
+ $this->installEntitySchema('commerce_product_variation');
+ $this->installEntitySchema('commerce_order');
+ $this->installEntitySchema('commerce_order_item');
+ $this->installEntitySchema('commerce_license');
+ $this->installConfig('system');
+ $this->installConfig('commerce_order');
+ $this->installConfig('commerce_product');
+ $this->createUser();
+
+ $this->licenseStorage = $this->container->get('entity_type.manager')->getStorage('commerce_license');
+
+ // Create an order type for licenses which uses the fulfillment workflow.
+ $this->orderType = $this->createEntity('commerce_order_type', [
+ 'id' => 'license_order_type',
+ 'label' => $this->randomMachineName(),
+ 'workflow' => 'order_default',
+ ]);
+ commerce_order_add_order_items_field($this->orderType);
+
+ // Create an order item type that uses that order type.
+ $order_item_type = $this->createEntity('commerce_order_item_type', [
+ 'id' => 'license_order_item_type',
+ 'label' => $this->randomMachineName(),
+ 'purchasableEntityType' => 'commerce_product_variation',
+ 'orderType' => 'license_order_type',
+ 'traits' => ['commerce_license_order_item_type'],
+ ]);
+ $this->traitManager = \Drupal::service('plugin.manager.commerce_entity_trait');
+ $trait = $this->traitManager->createInstance('commerce_license_order_item_type');
+ $this->traitManager->installTrait($trait, 'commerce_order_item', $order_item_type->id());
+
+ // Create a product variation type with the license trait, using our order
+ // item type.
+ $this->variationType = $this->createEntity('commerce_product_variation_type', [
+ 'id' => 'license_pv_type',
+ 'label' => $this->randomMachineName(),
+ 'orderItemType' => 'license_order_item_type',
+ 'traits' => ['commerce_license'],
+ ]);
+ $trait = $this->traitManager->createInstance('commerce_license');
+ $this->traitManager->installTrait($trait, 'commerce_product_variation', $this->variationType->id());
+
+ $this->variationType->setThirdPartySetting('commerce_license', 'allow_renewal', TRUE);
+ $this->variationType->setThirdPartySetting('commerce_license', 'interval', '1');
+ $this->variationType->setThirdPartySetting('commerce_license', 'period', 'month');
+ $this->variationType->setThirdPartySetting('commerce_license', 'activate_on_place', TRUE);
+ $this->variationType->save();
+
+ // Create a product variation which grants a license.
+ $this->variation = $this->createEntity('commerce_product_variation', [
+ 'type' => 'license_pv_type',
+ 'sku' => $this->randomMachineName(),
+ 'price' => [
+ 'number' => 999,
+ 'currency_code' => 'USD',
+ ],
+ 'license_type' => [
+ 'target_plugin_id' => 'renewable',
+ 'target_plugin_configuration' => [],
+ ],
+ // Use the rolling interval expiry plugin as it's simple.
+ 'license_expiration' => [
+ 'target_plugin_id' => 'rolling_interval',
+ 'target_plugin_configuration' => [
+ 'interval' => [
+ 'interval' => '1',
+ 'period' => 'year',
+ ],
+ ],
+ ],
+ ]);
+
+ // We need a product too otherwise tests complain about the missing
+ // backreference.
+ $this->createEntity('commerce_product', [
+ 'type' => 'default',
+ 'title' => $this->randomMachineName(),
+ 'stores' => [$this->store],
+ 'variations' => [$this->variation],
+ ]);
+ $this->reloadEntity($this->variation);
+ $this->variation->save();
+
+ // Create a product variation with a non-renewable license.
+ $this->nonRenewableVariationType = $this->createEntity('commerce_product_variation_type', [
+ 'id' => 'license_nrpv_type',
+ 'label' => $this->randomMachineName(),
+ 'orderItemType' => 'license_order_item_type',
+ 'traits' => ['commerce_license'],
+ ]);
+ $this->traitManager->installTrait($trait, 'commerce_product_variation', $this->nonRenewableVariationType->id());
+
+ $this->nonRenewableVariationType->setThirdPartySetting('commerce_license', 'allow_renewal', FALSE);
+ $this->nonRenewableVariationType->save();
+
+ // Create a product variation which grants a license.
+ $this->nonRenewableVariation = $this->createEntity('commerce_product_variation', [
+ 'type' => 'license_nrpv_type',
+ 'sku' => $this->randomMachineName(),
+ 'price' => [
+ 'number' => 999,
+ 'currency_code' => 'USD',
+ ],
+ 'license_type' => [
+ 'target_plugin_id' => 'renewable',
+ 'target_plugin_configuration' => [],
+ ],
+ // Use the rolling interval expiry plugin as it's simple.
+ 'license_expiration' => [
+ 'target_plugin_id' => 'rolling_interval',
+ 'target_plugin_configuration' => [
+ 'interval' => [
+ 'interval' => '1',
+ 'period' => 'year',
+ ],
+ ],
+ ],
+ ]);
+
+ // We need a product too otherwise tests complain about the missing
+ // backreference.
+ $this->createEntity('commerce_product', [
+ 'type' => 'default',
+ 'title' => $this->randomMachineName(),
+ 'stores' => [$this->store],
+ 'variations' => [$this->variation],
+ ]);
+ $this->reloadEntity($this->variation);
+ $this->variation->save();
+
+ // Create a user to use for orders.
+ $this->user = $this->createUser();
+
+ $this->installCommerceCart();
+ $this->store = $this->createStore();
+
+ // Create a license in the active state.
+ $this->license = $this->createEntity('commerce_license', [
+ 'type' => 'renewable',
+ 'state' => 'active',
+ 'product_variation' => $this->variation->id(),
+ 'uid' => $this->user->id(),
+ // 06/01/2015 @ 1:00pm (UTC).
+ 'expires' => '1433163600',
+ 'expiration_type' => [
+ 'target_plugin_id' => 'rolling_interval',
+ 'target_plugin_configuration' => [
+ 'interval' => [
+ 'interval' => '1',
+ 'period' => 'year',
+ ],
+ ],
+ ],
+ ]);
+
+ // Create a non renewable license in the active state.
+ $this->nonRenewableLicense = $this->createEntity('commerce_license', [
+ 'type' => 'renewable',
+ 'state' => 'active',
+ 'product_variation' => $this->nonRenewableVariation->id(),
+ 'uid' => $this->user->id(),
+ // 06/01/2015 @ 1:00pm (UTC).
+ 'expires' => '1433163600',
+ 'expiration_type' => [
+ 'target_plugin_id' => 'rolling_interval',
+ 'target_plugin_configuration' => [
+ 'interval' => [
+ 'interval' => '1',
+ 'period' => 'year',
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * Tests that a license can't be purchased outside the renewable window.
+ */
+ public function testRenewOutsideRenewalWindow() {
+ // Mock the current time service.
+ $expiration_time_outside_window = strtotime('- 2 months', $this->license->getExpiresTime());
+
+ $mock_builder = $this->getMockBuilder('Drupal\Component\Datetime\TimeInterface')
+ ->disableOriginalConstructor();
+
+ $datetime_service = $mock_builder->getMock();
+ $datetime_service->expects($this->atLeastOnce())
+ ->method('getRequestTime')
+ ->willReturn($expiration_time_outside_window);
+ $this->container->set('datetime.time', $datetime_service);
+
+ // Add a product with license to the cart.
+ $cart_order = $this->container->get('commerce_cart.cart_provider')->createCart('license_order_type', $this->store, $this->user);
+ $this->cartManager = $this->container->get('commerce_cart.cart_manager');
+ $order_item = $this->cartManager->addEntity($cart_order, $this->variation);
+
+ // Assert the order item is NOT in the cart.
+ $this->assertFalse($cart_order->hasItem($order_item));
+ }
+
+ /**
+ * Tests that a license is extended when you repurchased it.
+ */
+ public function testRenewInRenewalWindow() {
+ // Mock the current time service.
+ $expiration_time_inside_window = strtotime('- 1 week', $this->license->getExpiresTime());
+
+ $mock_builder = $this->getMockBuilder('Drupal\Component\Datetime\TimeInterface')
+ ->disableOriginalConstructor();
+
+ $datetime_service = $mock_builder->getMock();
+ $datetime_service->expects($this->atLeastOnce())
+ ->method('getRequestTime')
+ ->willReturn($expiration_time_inside_window);
+ $this->container->set('datetime.time', $datetime_service);
+
+ // Add a product with license to the cart.
+ $cart_order = $this->container->get('commerce_cart.cart_provider')->createCart('license_order_type', $this->store, $this->user);
+ $this->cartManager = $this->container->get('commerce_cart.cart_manager');
+ $order_item = $this->cartManager->addEntity($cart_order, $this->variation);
+ $order_item = $this->reloadEntity($order_item);
+
+ // Check that the order item has the previous license.
+ $this->assertNotNull($order_item->license->entity, 'The order item has a license set on it.');
+ $this->assertEquals($this->license->id(), $order_item->license->entity->id(), 'The order item has a reference to the existing license.');
+
+ // Assert the order item IS IN the cart.
+ $this->assertTrue($cart_order->hasItem($order_item), 'The order item IS IN the cart.');
+
+ // Take the order through checkout.
+ $this->completeLicenseOrderCheckout($cart_order);
+
+ // Reload the entity because it has been changed.
+ $this->license = $this->reloadEntity($this->license);
+
+ $this->assertEquals(date(DATE_ATOM, strtotime('+1 year', 1433163600)), date(DATE_ATOM, $this->license->getExpiresTime()), 'The license has been extended for a year.');
+ }
+
+ /**
+ * Tests that a license is active after removing renewing product from cart.
+ */
+ public function testRemovingProductFromCart() {
+ $initial_expiration_time = $this->license->getExpiresTime();
+
+ // Mock the current time service.
+ $expiration_time_inside_window = strtotime('- 1 week', $initial_expiration_time);
+
+ $mock_builder = $this->getMockBuilder('Drupal\Component\Datetime\TimeInterface')
+ ->disableOriginalConstructor();
+
+ $datetime_service = $mock_builder->getMock();
+ $datetime_service->expects($this->atLeastOnce())
+ ->method('getRequestTime')
+ ->willReturn($expiration_time_inside_window);
+ $this->container->set('datetime.time', $datetime_service);
+
+ // Add a product with license to the cart.
+ $cart_order = $this->container->get('commerce_cart.cart_provider')->createCart('license_order_type', $this->store, $this->user);
+ $this->cartManager = $this->container->get('commerce_cart.cart_manager');
+ $order_item = $this->cartManager->addEntity($cart_order, $this->variation);
+ $order_item = $this->reloadEntity($order_item);
+ $cart_order = $this->reloadEntity($cart_order);
+
+ // Check that the order item has the previous license.
+ $this->assertNotNull($order_item->license->entity, 'The order item has a license set on it.');
+ $this->assertEquals($this->license->id(), $order_item->license->entity->id(), 'The order item has a reference to the existing license.');
+
+ // Assert the order item IS IN the cart.
+ $this->assertTrue($cart_order->hasItem($order_item), 'The order item IS IN the cart.');
+
+ // Test that the license is in renewal_in_progress state.
+ $this->assertEquals('renewal_in_progress', $this->license->getState()->value, 'The license is in "renewal in progress" state.');
+
+ // Remove the item from the cart.
+ $order_item = $this->reloadEntity($order_item);
+ $this->cartManager->removeOrderItem($cart_order, $order_item);
+
+ // Assert the order item is NOT in the cart.
+ $this->assertFALSE($cart_order->hasItem($order_item), 'The order item is NOT in the cart.');
+
+ // Reload the entity because it may have been changed.
+ $this->license = $this->reloadEntity($this->license);
+
+ // Test that the license is back to active state without the expiration date
+ // extended.
+ $this->assertEquals('active', $this->license->getState()->value, 'The license is back to the "active" state.');
+ $this->assertEquals($initial_expiration_time, $this->license->getExpiresTime(), 'The license has still the same expiration time.');
+ }
+
+ /**
+ * Tests that a non renewable license can't be purchased if still active.
+ */
+ public function testNonRenewableLicense() {
+ // Add a product with license to the cart.
+ $cart_order = $this->container->get('commerce_cart.cart_provider')->createCart('license_order_type', $this->store, $this->user);
+ $this->cartManager = $this->container->get('commerce_cart.cart_manager');
+ $order_item = $this->cartManager->addEntity($cart_order, $this->nonRenewableVariation);
+
+ // Assert the order item is NOT in the cart.
+ $this->assertFalse($cart_order->hasItem($order_item));
+ }
+
+ /**
+ * Creates and saves a new entity.
+ *
+ * @param string $entity_type
+ * The entity type to be created.
+ * @param array $values
+ * An array of settings.
+ * Example: 'id' => 'foo'.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * A new, saved entity.
+ */
+ protected function createEntity($entity_type, array $values) {
+ /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
+ $storage = \Drupal::service('entity_type.manager')->getStorage($entity_type);
+ $entity = $storage->create($values);
+ $status = $entity->save();
+ $this->assertEquals(SAVED_NEW, $status, new FormattableMarkup('Created %label entity %type.', [
+ '%label' => $entity->getEntityType()->getLabel(),
+ '%type' => $entity->id(),
+ ]));
+ // The newly saved entity isn't identical to a loaded one, and would fail
+ // comparisons.
+ $entity = $storage->load($entity->id());
+
+ return $entity;
+ }
+
+}
diff --git a/tests/src/Kernel/LicenseCronExpiryTest.php b/tests/src/Kernel/LicenseCronExpiryTest.php
index a5ec07d..5a9e700 100644
--- a/tests/src/Kernel/LicenseCronExpiryTest.php
+++ b/tests/src/Kernel/LicenseCronExpiryTest.php
@@ -15,6 +15,7 @@ use Drupal\Tests\commerce_order\Kernel\OrderKernelTestBase;
class LicenseCronExpiryTest extends OrderKernelTestBase {
use AssertMailTrait;
+ use ArraySubsetAsserts;
/**
* The number of seconds in one day.
diff --git a/tests/src/Kernel/LicenseOrderCompletionTestTrait.php b/tests/src/Kernel/LicenseOrderCompletionTestTrait.php
new file mode 100644
index 0000000..09890cc
--- /dev/null
+++ b/tests/src/Kernel/LicenseOrderCompletionTestTrait.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tests\commerce_license\Kernel;
+
+use Drupal\commerce_order\Entity\OrderInterface;
+
+/**
+ * Provides a helper method to take an license order to completion.
+ */
+trait LicenseOrderCompletionTestTrait {
+
+ /**
+ * Takes a cart order through to the end of checkout.
+ *
+ * This uses the states appropriate to the order's workflow to ensure that
+ * the license will be created.
+ *
+ * @param \Drupal\commerce_order\Entity\OrderInterface $cart_order
+ * The order.
+ */
+ protected function completeLicenseOrderCheckout(OrderInterface $cart_order) {
+ $workflow = $cart_order->getState()->getWorkflow();
+
+ // In all cases, place the order.
+ $cart_order->getState()->applyTransition($workflow->getTransition('place'));
+ $cart_order->save();
+
+ // The order is now either in state:
+ // - 'complete', if its workflow is 'order_default'
+ // - 'fulfillment', if its workflow is 'order_fulfillment'
+
+ // Fulfil the order if it has that transtion.
+ $fulfil_transition = $workflow->getTransition('fulfill');
+ if ($fulfil_transition) {
+ $cart_order->getState()->applyTransition($fulfil_transition);
+ $cart_order->save();
+ }
+ }
+
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment