Last active
March 7, 2023 14:16
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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