(I could have sworn there was an issue for this, but I can only find a 7.x one that I don't want to hijack.)

Rough notes:

- the availability checker for ownership of a grantable gets in the way
- we probably still need to prevent re-purchase of a license product early on in the license's lifetime. That suggests a setting on the PV to say 'renewals may be purchased within interval X from expiry'.
- the availability checker thus needs to know whether there is a license, as well as the grantable. The logic becomes:

 - got license? yes:
    - is it unlimited? may not purchase
    - are you in the renewal period? yes: may purchase
 - got license? no:
    - do you have the grantable? yes: may not purchase

- the cart should show a clear message that the user is purchasing a renewal, and what date they are extending to
- something needs to place the existing license on the order item in the new order
- the order sync class needs to know it's a renewal, and at the point where it would normally activate, extend instead
- the code that deletes a license when an order item is deleted needs to account for this process, and NOT delete the license
- tests. all the tests.

Tests that needs to be done:

  • For a non renewable license: you can't renew
  • For a renewable license:
    1. buy a license
    2. complete the order
    3. buy the same license in the renewable window
    4. complete the order
    5. see that the expires has been augmented of the license duration
  • For a renewable license: test that you can't renew outsite the renewable window
  • For a renewable license:
    1. buy a license
    2. complete the order
    3. buy the same license in the renewable window
    4. stop at the cart state
    5. remove the product from the cart
    6. see that the license in the active state and the expire time has been untouched
CommentFileSizeAuthor
#148 allow-repurchase-diferent-PV-2943888-148.patch5.02 KBjunaidpv
#141 reroll-commerce_license-allow-renewal-2943888-141.patch50.12 KBrobotjox
#138 reroll-commerce_license-allow-renewal-2943888-138.patch105.45 KBrobotjox
#135 interdiff_134_135.txt1017 bytesjoekers
#135 commerce_license-allow-renewal-2943888-135.patch51.95 KBjoekers
#134 commerce_license-allow-renewal-2943888-134.patch51.86 KBjoekers
#132 commerce_license-allow-renewal-2943888-132.patch51.87 KBpmagunia
#131 commerce_license-allow-renewal-2943888-131.patch51.87 KBpmagunia
#130 commerce_license-allow-renewal-2943888-130.patch50.75 KBpmagunia
#130 commerce_license-allow-renewal-2943888-130.patch50.75 KBpmagunia
#124 commerce_license-allow-renewal-2943888-124.patch48.33 KBrobcarr
#121 commerce_license-allow-renewal-2943888-121.patch48.27 KBrobcarr
#115 commerce_license-allow_renewal-2943888-115.patch49.89 KBjunaidpv
#114 commerce_license-allow_renewal-2943888-114.patch49.29 KBjunaidpv
#110 commerce_license-allow_renewal-2943888-110.patch46.26 KBrobcarr
#109 commerce_license-allow_renewal-2943888-109.patch44.81 KBrobcarr
#108 commerce_license-allow_renewal-2943888-108.patch22.67 KBrobcarr
#104 commerce-license-ability-to-re-purchase-a-license-to-extend-it-102.patch47.24 KBrobcarr
#101 commerce_license-allow_renewal-2943888-101.patch22.32 KBrobcarr
#100 commerce_license-allow_renewal-2943888-100.patch22.37 KBrobcarr
#92 interdiff_86-91.txt8.99 KBfy1128
#91 commerce_license-allow_renewal.patch82.54 KBfy1128
#86 interdiff_76-86.txt4.26 KBpmagunia
#86 commerce_license-allow_renewal-2943888-86.patch80.98 KBpmagunia
#83 interdiff_76-83.txt3.89 KBpmagunia
#83 commerce_license-allow_renewal-2943888-83.patch80.61 KBpmagunia
#81 interdiff_76-81.txt26.11 KBpmagunia
#81 commerce_license-allow_renewal-2943888-81.patch58.22 KBpmagunia
#76 2943888-commerce_license-allow_renewal-76.patch76.63 KBKojo Unsui
#75 2943888-commerce_license-allow_renewal-75.patch73.95 KBKojo Unsui
#74 2943888-commerce_license-allow_renewal.patch51.22 KBKojo Unsui
#64 interdiff-2943888-64-62.txt5.95 KBGrimreaper
#64 commerce_license-allow_renewal-2943888-64.patch41.96 KBGrimreaper
#62 interdiff-2943888-62-60.txt885 bytesGrimreaper
#62 commerce_license-allow_renewal-2943888-62.patch39.76 KBGrimreaper
#60 commerce_license-allow_renewal-2943888-60.patch39.71 KBGrimreaper
#60 interdiff-2943888-60-57.txt5.41 KBGrimreaper
#57 interdiff-2943888-57-55.txt8.18 KBGrimreaper
#57 commerce_license-allow_renewal-2943888-57.patch38.66 KBGrimreaper
#55 interdiff-2943888-52-55.txt14.2 KBGrimreaper
#55 commerce_license-allow_renewal-2943888-55.patch33.27 KBGrimreaper
#52 interdiff-2943888-50-52.txt1.39 KBGrimreaper
#52 commerce_license-allow_renewal-2943888-52.patch21.04 KBGrimreaper
#50 interdiff-2943888-49-50.txt6.13 KBGrimreaper
#50 commerce_license-allow_renewal-2943888-50.patch21.01 KBGrimreaper
#49 interdiff-2943888-46-49.txt5.02 KBGrimreaper
#49 commerce_license-allow_renewal-2943888-49.patch17.9 KBGrimreaper
#47 interdiff-2943888-44-46.txt1.29 KBGrimreaper
#47 commerce_license-allow_renewal-2943888-46.patch14.12 KBGrimreaper
#44 interdiff-2943888-42-44.txt4.78 KBGrimreaper
#44 commerce_license-allow_renewal-2943888-44.patch14.11 KBGrimreaper
#42 commerce_license-allow_renewal-2943888-42.patch13.88 KBGrimreaper
#41 commerce_license-allow_renewal-2943888-41.patch13.98 KBGrimreaper
#22 renew-before-expiration.patch19.19 KBpmagunia
#20 renew-before-expiration.patch19.19 KBpmagunia
#16 renew-active-license.patch10.2 KBpmagunia
#14 p1.patch2.99 KBpmagunia
#6 renew-license-before-expiration.patch4.99 KBpmagunia
#4 renew-license-before-expiration.patch4.91 KBpmagunia
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

joachim created an issue. See original summary.

Grimreaper’s picture

Hello,

First, thanks for your work on this module. I have just retested the 8.x-2.0-alpha8 and it works well.

I am also interested in this feature request for the French Drupal association https://github.com/Drupal-FR/site-drupalfr.

I don't think we will go on the commerce recurring module because of what I see it is more for automated billing and more complex. Also I think having people manually buying their membership before expiration should be a possibility.

宁皓网_王皓’s picture

Thank you for the really awesome work : )

pmagunia’s picture

Here is a patch to get started with. It's just a rough draft but it did work in my use case.

It requires the patch from Recurring Period which I just submitted which is referenced here.

Status: Needs review » Needs work

The last submitted patch, 4: renew-license-before-expiration.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

pmagunia’s picture

Status: Needs work » Needs review
FileSize
4.99 KB

Trying to get the test passed.

Status: Needs review » Needs work

The last submitted patch, 6: renew-license-before-expiration.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

joachim’s picture

Thanks for having a go at this. It's always nice to see new people working with this module :)

There are a few problems with your approach though:

> It requires the patch from Recurring Period which I just submitted which is referenced here.

I had a quick look at that patch. It shouldn't be in that module -- RP should not know anything about licenses. It's just a system to produce recurring dates on a schedule.

  1. +++ b/src/Entity/License.php
    @@ -93,24 +93,50 @@ class License extends ContentEntityBase implements LicenseInterface {
    +           // The user has this license already. Let's revoke that license
    +           // before extending a new one.
    +           foreach ($license_ids as $license_id) {
    

    I think it makes more sense to extend the license entity rather than create a new one.

  2. +++ b/src/Entity/License.php
    @@ -367,6 +408,10 @@ class License extends ContentEntityBase implements LicenseInterface {
    +    $fields['product_variation'] = BaseFieldDefinition::create('integer')
    

    There's already this field! Are you maybe working with an old version of the module?

  3. +++ b/src/Entity/License.php
    @@ -93,24 +93,50 @@ class License extends ContentEntityBase implements LicenseInterface {
    +        $this->getTypePlugin()->grantLicense($this);
    +
    +        // Check if the user already has a license so that it can renew.
    

    If the user already has a license, then grantLicense shouldn't be called.

Overall, I suggest you look at the outline I posted in the issue summary and implement it that way (unless you spot mistakes in my plan, of course!).

pmagunia’s picture

Thank you for your quick feedback!

I will have another go at this.

The reason I usedBundleFieldDefinition is for the life of me I couldn’t get the value of product variation in the License Class in the presave method.

Also, Above you mention the grantable:

- got license? no:
    - do you have the grantable? yes: may not purchase

Can you please explain what exactly is the grantable?

joachim’s picture

> I couldn’t get the value of product variation in the License Class in the presave method.

I have a patch I've been working on for another issue which adds a getter for that. I'll try to remember to commit just that part of it tomorrow when I'm at my work machine so you can use that.

> Can you please explain what exactly is the grantable?

The 'grantable' is the thing the user owns or has access to because they have a license. So, e.g., a role, or an OG membership entity, or a field value on a node. There is an interface for checking that on license plugins.

joachim’s picture

> I have a patch I've been working on for another issue which adds a getter for that. I'll try to remember to commit just that part of it tomorrow when I'm at my work machine so you can use that.

Just pushed that commit: 20a22c6b25a8ed42b286c5a527cbd5edf18d8b31

pmagunia’s picture

@joachim Sorry it has taken me awhile to get back to you. Thank you for committing that getter.

One question I had is where should I add the checkbox and date field to allow a renewal within a specified time period?

I thought the the form on the Product type setting would be a good place to add it.

joachim’s picture

> I thought the the form on the Product type setting would be a good place to add it.

IIRC we already add some settings to the product variation type? In which case, that might be a good place, as the work of saving the extra options is already there.

pmagunia’s picture

Yes, I think you are right. It is the product variation type. I was able to get this together tonite.

Its not a full patch but something to get started with. For some reason I can't save the interval value.

I wanted to hide the interval when allow_renewal was unchecked but its it not being hidden.

If anyone else would like to jump in feel free.

joachim’s picture

  1. +++ b/src/FormAlter/ProductVariationTypeFormAlter.php
    @@ -83,6 +83,29 @@ class ProductVariationTypeFormAlter {
    +      '#title' => t("Allow Renewal Before Expiration"),
    

    Use sentence case rather than title case for these.

  2. +++ b/src/FormAlter/ProductVariationTypeFormAlter.php
    @@ -83,6 +83,29 @@ class ProductVariationTypeFormAlter {
    +        "Option to allow the license to be renewed before expiration."
    

    The description is just repeating the label. Say something like: "Allows a customer to renew their license by re-purchasing the product for it."

  3. +++ b/src/FormAlter/ProductVariationTypeFormAlter.php
    @@ -83,6 +83,29 @@ class ProductVariationTypeFormAlter {
    +        "Allow the license to be renewed before expiration when this amount of time is left before the expiration date."
    

    "The interval before the license's expiry during which re-purchase is allowed. Prior to this interval, re-purchase is blocked, as normal."

  4. +++ b/src/FormAlter/ProductVariationTypeFormAlter.php
    @@ -83,6 +83,29 @@ class ProductVariationTypeFormAlter {
    +          ':input[name="allow_renewal"]' => ['checked' => TRUE],
    

    The name value here is wrong -- you need the full chain of parents of the form element.

pmagunia’s picture

Thank you for those suggestions. I've incorporated them into this new patch.

I think everything works except that when a license is renewed, a duplicate license is issued.

I've tried deleting it with $this->delete() but then I get an error message that a string is not translatable.

I believe the Renew column is being set correctly on the original license though.

Any suggestions would be welcome.

joachim’s picture

Glad to hear you've got the UI working!

  1. +++ b/src/Entity/License.php
    @@ -79,7 +79,8 @@ class License extends ContentEntityBase implements LicenseInterface {
    +    // license is being renewed
    

    I don't think that is true, as you're not changing the conditions on this if() block.

  2. 
    Note also that comments need to be wrapped to 80 lines and written as sentences, with full stops at the end.
    +++ b/src/Entity/License.php
    @@ -91,32 +92,43 @@ class License extends ContentEntityBase implements LicenseInterface {
    +      // Check if the user already has an active license so that it can renew.
    +      $query = \Drupal::entityQuery('commerce_license')
    +        ->condition('state', 'active')
    +        ->condition('uid', $this->getOwnerId())
    +        ->condition('product_variation', $this->getPurchasedEntity()->id());
    +      if ($license = $query->execute()) {
    

    This isn't the right approach at all. We shouldn't be getting this far!

    When the user places the order, you need to find the existing license, and attach it to the order item.

    You are then working with just one license, and here you simply need to extend its expiry date.

  3. +++ b/src/Entity/License.php
    @@ -91,32 +92,43 @@ class License extends ContentEntityBase implements LicenseInterface {
    +        } else {
    

    Looks like your IDE is breaking Drupal coding standards...

  4. +++ b/src/LicenseAvailabilityCheckerExistingRights.php
    @@ -108,7 +108,16 @@ class LicenseAvailabilityCheckerExistingRights implements AvailabilityCheckerInt
    +    $license_type = $entity->license_expiration->first()->getValue('target_plugin_id')['target_plugin_id']; ¶
    

    That's not the license type, it's the expiry type. The license type is something like 'role'.

pmagunia’s picture

I had a quick question regarding one set of your comments above.

When the user places the order, you need to find the existing license, and attach it to the order item.

You are then working with just one license, and here you simply need to extend its expiry date.

I was going to move this logic to the LicenseAvailabilityCheckerExistingRights.php file. I'm guessing the existing license gets attached to $entity or $this .

If so, which class variable do we attach it to or replace?

joachim’s picture

> I was going to move this logic to the LicenseAvailabilityCheckerExistingRights.php file. I'm guessing the existing license gets attached to $entity or $this .

No, in that class, PurchasableEntityInterface $entity is the product variation. That is not something that the customer can change by making a purchase!

The license gets created in LicenseOrderSyncSubscriber and set on the order item when the order is placed. But LicenseOrderSyncSubscriber also allows for the possibility of the license already being set on the order item.

So you need to step in earlier than that, to 1. load the existing license, and 2. set it on the order item. So probably on the add to cart event.

See also these items in my rough outline above:

- the cart should show a clear message that the user is purchasing a renewal, and what date they are extending to
- something needs to place the existing license on the order item in the new order
- the order sync class needs to know it's a renewal, and at the point where it would normally activate, extend instead
- the code that deletes a license when an order item is deleted needs to account for this process, and NOT delete the license

pmagunia’s picture

Status: Needs work » Needs review
FileSize
19.19 KB

Had another go at it. This was working on my server though I haven't added any functionality to account for deletions.

I wanted to throw something out there though just to make sure I was going in the right direction.

Status: Needs review » Needs work

The last submitted patch, 20: renew-before-expiration.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

pmagunia’s picture

Status: Needs work » Needs review
FileSize
19.19 KB

Had to pull the changes from dev first.

Status: Needs review » Needs work

The last submitted patch, 22: renew-before-expiration.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

pmagunia’s picture

@joachim, did you have comments about my last patch?

I know it failed testing but I was wondering if I was on the right track or if you had any other comments.

joachim’s picture

Sorry for the delay in getting back to you. This is a very quick pre-breakfast review, apologies if I've skimmed over things.

  1. +++ b/commerce_license.services.yml
    @@ -26,14 +26,6 @@ services:
    -  commerce_license.license_availability_checker_existing:
    -    class: Drupal\commerce_license\LicenseAvailabilityCheckerExistingRights
    -    arguments:
    -      - '@current_user'
    -      - '@entity_type.manager'
    -    tags:
    -      - { name: commerce.availability_checker }
    -
    

    Why is this getting removed?

  2. +++ b/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
    @@ -37,7 +37,7 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
    @@ -55,6 +55,102 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
    
    @@ -55,6 +55,102 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
           return;
         }
     
    +    // Load these for use later
    +    $customer_uid = $order_item->getOrder()->getCustomerId();
    +    $license_type_plugin = $event->getEntity()->license_type->first()->getTargetInstance();
    

    We're adding a lot of functionality here, so the class name becomes incorrect.
    The code is a bit messy here, and needs a fair bit of cleaning up -- see comments below.

  3. +++ b/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
    @@ -55,6 +55,102 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
    +    // Check if the user doesn't have the license. If not, allow product to be added to cart
    +    if (!$existing_rights_result->hasExistingRights()) {
    

    Comment and code don't match: the existing rights checker does not check for a license.

  4. +++ b/src/EventSubscriber/LicenseMultiplesCartEventSubscriber.php
    @@ -55,6 +55,102 @@ class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {
    +        if ($customer->id() == \Drupal::currentUser()->id()) {
    +          $rights_check_message = $existing_rights_result->getOwnerUserMessage();
    +        }
    +        else {
    +          $rights_check_message = $existing_rights_result->getOtherUserMessage();
    +        }
    

    This code doesn't seem to be useful - nothing's being done with these strings.

  5. +++ b/src/EventSubscriber/LicenseOrderSyncSubscriber.php
    @@ -101,10 +102,31 @@ class LicenseOrderSyncSubscriber implements EventSubscriberInterface {
    +        $license->save();
    

    Are you sure this is the right place to do this? There is a setting for licenses to be activated on order place rather than validation.

  6. +++ /dev/null
    @@ -1,130 +0,0 @@
    -<?php
    -
    -namespace Drupal\commerce_license;
    -
    -use Drupal\Core\Entity\EntityTypeManagerInterface;
    -use Drupal\Core\Session\AccountProxyInterface;
    -use Drupal\commerce\AvailabilityCheckerInterface;
    -use Drupal\commerce\Context;
    -use Drupal\commerce\PurchasableEntityInterface;
    -use Drupal\commerce_recurring\RecurringOrderManager;
    -use Drupal\commerce_product\Entity\ProductVariationInterface;
    -use Drupal\commerce_license\Plugin\Commerce\LicenseType\ExistingRightsFromConfigurationCheckingInterface;
    

    Why is this file getting deleted?

joachim’s picture

Ok, having now had breakfast, here's some still pretty rough ideas:

- I'd like LicenseAvailabilityCheckerExistingRights to remain, but obviously, it needs to know to step out of the way if the product is configured to allow renewals.
- LicenseMultiplesCartEventSubscriber should be unchanged. It has a simple job to do, involving only the count of things in the cart.
- we need a new event subscriber, to act on onCartEntityAdd. This is what should check if a repurchase is allowed, and if so, it should take care of finding the existing license and setting it on the $order_item.
- we probably need to consider what happens if a user adds a renewable product to their cart, then goes away for days, and comes back to their cart after expiry, and other similar considerations. That's probably handled by checking order refresh.
- LicenseOrderSyncSubscriber needs to find the license on the order item, and know to extend the license. This gets a bit hairy, because I don't think we can or should allow the 'Activate the license in the 'place' transition' system to be used in this case -- because what do you do if the order is cancelled? You'd need to subtract a year from the expiry and it just gets too messy. Also, the whole point of 'Activate the license in the 'place' transition' was to give the user access immediately for payment gateways that have a delay, and in a renewal, the user already has access. So basically, the logic is that if the license is already active, AND the order state is complete, then extend the license period.
- extending the license period should be done with a new method on the License entity, rather than making the expiry date method public.

Grimreaper’s picture

Hello,

Thanks all for your work on this issue, I have tested patch from comment #22.

I works but there are some warnings.

On the complete step when it is the first time a license is created for the user for a product:

Notice: Undefined offset: 0 in Drupal\commerce_license\EventSubscriber\LicenseOrderSyncSubscriber->onCartOrderTransition() (line 114 of modules/contrib/commerce_license/src/EventSubscriber/LicenseOrderSyncSubscriber.php).
Drupal\commerce_license\EventSubscriber\LicenseOrderSyncSubscriber->onCartOrderTransition(Object, 'commerce_order.place.pre_transition', Object)
call_user_func(Array, Object, 'commerce_order.place.pre_transition', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('commerce_order.place.pre_transition', Object) (Line: 339)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->dispatchTransitionEvent('pre_transition') (Line: 308)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->preSave() (Line: 233)
Drupal\Core\Field\FieldItemList->delegateMethod('preSave') (Line: 191)
Drupal\Core\Field\FieldItemList->preSave() (Line: 768)
Drupal\Core\Entity\ContentEntityStorageBase->invokeFieldMethod('preSave', Object) (Line: 718)
Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('presave', Object) (Line: 88)
Drupal\commerce\CommerceContentEntityStorage->invokeHook('presave', Object) (Line: 435)
Drupal\Core\Entity\EntityStorageBase->doPreSave(Object) (Line: 587)
Drupal\Core\Entity\ContentEntityStorageBase->doPreSave(Object) (Line: 89)
Drupal\commerce_order\OrderStorage->doPreSave(Object) (Line: 389)
Drupal\Core\Entity\EntityStorageBase->save(Object) (Line: 820)
Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object) (Line: 391)
Drupal\Core\Entity\Entity->save() (Line: 130)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowBase->redirectToStep('complete') (Line: 173)
Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentProcess->buildPaneForm(Array, Object, Array) (Line: 556)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase->buildForm(Array, Object, 'payment')
call_user_func_array(Array, Array) (Line: 518)
Drupal\Core\Form\FormBuilder->retrieveForm('commerce_checkout_flow_multistep_default', Object) (Line: 275)
Drupal\Core\Form\FormBuilder->buildForm('commerce_checkout_flow_multistep_default', Object) (Line: 216)
Drupal\Core\Form\FormBuilder->getForm(Object, 'payment') (Line: 94)
Drupal\commerce_checkout\Controller\CheckoutController->formPage(Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 582)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 99)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 78)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 666)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

Notice: Trying to get property of non-object in Drupal\commerce_license\EventSubscriber\LicenseOrderSyncSubscriber->onCartOrderTransition() (line 114 of modules/contrib/commerce_license/src/EventSubscriber/LicenseOrderSyncSubscriber.php).
Drupal\commerce_license\EventSubscriber\LicenseOrderSyncSubscriber->onCartOrderTransition(Object, 'commerce_order.place.pre_transition', Object)
call_user_func(Array, Object, 'commerce_order.place.pre_transition', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('commerce_order.place.pre_transition', Object) (Line: 339)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->dispatchTransitionEvent('pre_transition') (Line: 308)
Drupal\state_machine\Plugin\Field\FieldType\StateItem->preSave() (Line: 233)
Drupal\Core\Field\FieldItemList->delegateMethod('preSave') (Line: 191)
Drupal\Core\Field\FieldItemList->preSave() (Line: 768)
Drupal\Core\Entity\ContentEntityStorageBase->invokeFieldMethod('preSave', Object) (Line: 718)
Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('presave', Object) (Line: 88)
Drupal\commerce\CommerceContentEntityStorage->invokeHook('presave', Object) (Line: 435)
Drupal\Core\Entity\EntityStorageBase->doPreSave(Object) (Line: 587)
Drupal\Core\Entity\ContentEntityStorageBase->doPreSave(Object) (Line: 89)
Drupal\commerce_order\OrderStorage->doPreSave(Object) (Line: 389)
Drupal\Core\Entity\EntityStorageBase->save(Object) (Line: 820)
Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object) (Line: 391)
Drupal\Core\Entity\Entity->save() (Line: 130)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowBase->redirectToStep('complete') (Line: 173)
Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentProcess->buildPaneForm(Array, Object, Array) (Line: 556)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase->buildForm(Array, Object, 'payment')
call_user_func_array(Array, Array) (Line: 518)
Drupal\Core\Form\FormBuilder->retrieveForm('commerce_checkout_flow_multistep_default', Object) (Line: 275)
Drupal\Core\Form\FormBuilder->buildForm('commerce_checkout_flow_multistep_default', Object) (Line: 216)
Drupal\Core\Form\FormBuilder->getForm(Object, 'payment') (Line: 94)
Drupal\commerce_checkout\Controller\CheckoutController->formPage(Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 582)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 99)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 78)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 666)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

And when selecting a product on which you already have a license, there is still the message "Renewals of this license are not allowed before expiration" even though the product is added to the cart.

Is there anything I can do to help resolve the issue. I saw @joachim review. Maybe you can guide me a little bit to do the points.

Also a question, this new bahavior will be for all licences. Would it be better to have a settings per licence to allow this behavior?

joachim’s picture

> Also a question, this new bahavior will be for all licences. Would it be better to have a settings per licence to allow this behavior?

No, the intention is definitely that it will be configured on each product, so not for all licenses.

> Is there anything I can do to help resolve the issue. I saw @joachim review. Maybe you can guide me a little bit to do the points.

If you have time to help push this forward that would be great, though @pmagunia might be planning to work on it too, so maybe wait until everyone's responded to my comments to coordinate.

Grimreaper’s picture

Thanks for your reply,

Ok, I though the behaviour would be set sitewide but if it can be set per product it's good.

Ok I'll wait for reply.

Grimreaper’s picture

Hi,

@joachim: I am at Drupal Europe. Would it be possible to sprint together (or at least having a brefing on this issue) to get this issue done?

joachim’s picture

Yup, I’ll be there, and I’m even doing a session on License!
I will be at the contribution day on Friday, so can help you with this then.

Grimreaper’s picture

Ok,

I am already there and I am registered on the spreadsheet: https://docs.google.com/spreadsheets/d/1OxtAsRs_SeKQynL5bJ_5AEmRInLdyG7m...

I will attend your session.

Until friday, can you give me direction on what needs to be done please? If you prefer to see that directly, it's ok. I have other subjects to sprint on.

joachim’s picture

If you want to get started today, have a look at comments #25 and #26. The most recent patch here is a good start, but makes a number of wrong turnings.

I get in late tonight, will be at the conference from tomorrow morning. Happy to have a chat at morning coffee or lunchtime!

joachim’s picture

Yay working on this at DrupalEurope!!!

Rough plan. These run in the following order.

- LicenseAvailabilityCheckerExistingRights needs to know to step out of the way if the product is configured to allow renewals. Otherwise it acts as normal to prevent a repurchase.
- New class: LicenseRenewalCartEventSubscriber. This queries for the user's existing license, and sets it on the order. This needs to run before LicenseOrderSyncSubscriber.
- LicenseOrderSyncSubscriber needs to know when and whether to extend the license.

Grimreaper’s picture

@joachim,

Ok, I found why the die() where not executed...

With the patch, the service LicenseAvailabilityCheckerExistingRights has been removed so after cancelling code deletion, we needed to clear the cache for Drupal to detect it exists again...

Grimreaper’s picture

- LicenseAvailabilityCheckerExistingRights needs to know to step out of the way if the product is configured to allow renewals. Otherwise it acts as normal to prevent a repurchase.

=> OK

In commerce_license/src/LicenseAvailabilityCheckerExistingRights.php :

  public function check(PurchasableEntityInterface $entity, $quantity, Context $context) {
    // Don't do an availability check on recurring orders.
    // Very ugly workaround for the lack of any information about the order at
    // this point.
    // See https://www.drupal.org/project/commerce/issues/2937041
    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    foreach ($backtrace as $backtrace_call) {
      // If the availability check is being done for an order being saved or
      // created by Commerce Recurring's RecurringOrderManager, then it's a
      // recurring order, and we shouldn't do anything.
      if (isset($backtrace_call['class']) && $backtrace_call['class'] == RecurringOrderManager::class) {
        return;
      }
    }

// Handle licence renewal.
    $product_variation_type_id = $entity->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;
//      $allow_renewal_window_interval = $product_variation_type->getThirdPartySetting('commerce_license', 'interval');
//      $allow_renewal_window_period = $product_variation_type->getThirdPartySetting('commerce_license', 'period');
    }

I will go to a session, but I need to discuss a little with you for the content of LicenseRenewalCartEventSubscriber.

joachim’s picture

We were talking about this step yesterday:

- New class: LicenseRenewalCartEventSubscriber. This queries for the user's existing license, and sets it on the order. This needs to run before LicenseOrderSyncSubscriber.
- LicenseOrderSyncSubscriber needs to know when and whether to extend the license.

The problem here is that LicenseOrderSyncSubscriber needs to know that the license on the order item should be renewed. But how?

- checking the license is already active isn't enough, as we come to LicenseOrderSyncSubscriber for multiple transitions, and some later ones can have a just-purchased license with active state
- we could save a flag on the entity, but that feels fiddly
- use the state field

Out of those, on reflection, I think using the state, as @Grimreaper suggested, is the cleanest. It does mean adding an extra state, but there is a symmetry to the initial purchase and the renewal purchase:

Initial purchase: license created in 'new' state -> goes to 'active' state
Renewal purchase: license moved to 'renewing' state -> goes to 'active' state

That does mean that a failed order needs to move 'renewing' back to 'active', same way a failed initial order has to clean up the license.

Grimreaper’s picture

@joachim,

Sorry, by "state", I meant the key/value API. Not a state in the state machine.

Do you still think a state of the state machine should be done?

joachim’s picture

Yes, a state on the license. I don't think using key/value is necessary, as we already have a content entity that can carry the data and is in the right place to do that.

joachim’s picture

Quick thoughts on refactoring of the code being worked on at the moment at DrupalEurope:

- move the query for loading an existing license for the current user & a given product variation to the LicenseStorage class / interface
- move the code to determine if a given license entity will allow a renewal repurchase to a method on the License entity

Grimreaper’s picture

Here is a WIP with comment 40 taken into account.

Grimreaper’s picture

Another patch where I worked into the LicenseRenewalCartEventSubscriber.

The right license is selected but after the addition to the cart. The cart is empty.

I think I will need to discuss a little bit with you @joachim.

joachim’s picture

Issue tags: +Needs tests

This is really starting to come together!

  1. +++ b/config/schema/commerce_license.schema.yml
    @@ -8,6 +8,12 @@ commerce_product.commerce_product_variation_type.*.third_party.commerce_license:
    +      type: text
    

    That type doesn't look right, as the form element for this is an 'interval'.

  2. +++ b/src/Entity/License.php
    @@ -476,4 +476,40 @@ class License extends ContentEntityBase implements LicenseInterface {
    +  public function isRenewable() {
    

    Change the method name to canRenew(), as isRenewable() makes me thing it's just about checking the 'allow_renewal' setting.

  3. +++ b/src/Entity/License.php
    @@ -476,4 +476,40 @@ class License extends ContentEntityBase implements LicenseInterface {
    +    $current_time = new \DateTimeImmutable();
    

    Use the current time service, so that we can mock it in tests.

  4. +++ b/src/Entity/License.php
    @@ -476,4 +476,40 @@ class License extends ContentEntityBase implements LicenseInterface {
    +    if ($renewal_window_start_time < $expiration_time && $expiration_time < $current_time) {
    

    Do we need to check expiration time is less than now?
    That makes me think though -- we should check the state is active at the top of this method.

  5. +++ b/src/Entity/LicenseInterface.php
    @@ -151,4 +151,12 @@ interface LicenseInterface extends EntityChangedInterface, EntityOwnerInterface
    +   * Checks if the license can be renewed.
    

    add 'at this time' to the end of the description.

  6. +++ b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
    @@ -0,0 +1,72 @@
    +    $existing_license = $this->licenseStorage->getExistingLicense($order_item->getPurchasedEntity(), $order_item->getOrder()->getCustomerId());
    

    Need to check renewal is allowed first.

  7. +++ b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
    @@ -0,0 +1,72 @@
    +      $order_item->set('license', $existing_license->id());
    

    I think we need to save the order item here.
    LicenseOrderSyncSubscriber saves order items when it makes changes to them.

  8. +++ b/src/LicenseStorage.php
    @@ -58,4 +58,23 @@ class LicenseStorage extends CommerceContentEntityStorage implements LicenseStor
    +  public function getExistingLicense(ProductVariationInterface $variation, $uid) {
    

    Let's take an $account parameter here.

    And also, we can bail early if $account->uid == 0, as licenses are never anonymous.

  9. +++ b/src/LicenseStorage.php
    @@ -58,4 +58,23 @@ class LicenseStorage extends CommerceContentEntityStorage implements LicenseStor
    +    if (!empty($existing_licenses)) {
    

    Can we change that variable name to have _ids at the end.

Grimreaper’s picture

Thanks for the review!

  1. Done: there is no type in the interval module to inherit from.
  2. Done
  3. I can't because I need a object that has the "sub" function to remove the timestamp interval. (and I didn't found how from the interval object print the corresponding timestamp)
  4. Done
  5. Done
  6. Done
  7. Done: but same result => no more item in the cart
  8. I have used an uid instead of an account object to be aligned with the createFromProductVariation method. Do you still want an account?
  9. Done
Grimreaper’s picture

OK, I found why I have an empty cart...

"You already have the member role. You may not purchase the adhésion product." => commerce_license/src/Plugin/Commerce/LicenseType/Role.php

Someone else is emptying it.

joachim’s picture

> I can't because I need a object that has the "sub" function to remove the timestamp interval. (and I didn't found how from the interval object print the corresponding timestamp)

Like this:

+    $current_time = new \DateTimeImmutable(\Drupal::time()->getRequestTime(););

> I have used an uid instead of an account object to be aligned with the createFromProductVariation method. Do you still want an account?

Hmm good point, not sure.

> "You already have the member role. You may not purchase the adhésion product." => commerce_license/src/Plugin/Commerce/LicenseType/Role.php

*sigh*... :)

Grimreaper’s picture

OK I found why LicenseAvailabilityCheckerExistingRights was still in the way. See attached patch and interdiff.

Grimreaper’s picture

    $renewal_window_start_time = new \DateTimeImmutable(date('r', \Drupal::time()->getRequestTime()));

Otherwise there will be a fatal error.

Now we need to see how to properly extends license expiration time.

Grimreaper’s picture

FileSize
17.9 KB
5.02 KB

Almost done.

The problem I have to extend the expiration timestamp is that in commerce_license/src/EventSubscriber/LicenseOrderSyncSubscriber.php, at the end of onCartOrderTransition method:

 // Attempt to activate and confirm the license.
      // TODO: This needs to be expanded for synchronizable licenses.
      // TODO: how does a license type plugin indicate that it's not able to
      // activate? And how do we notify the order at this point?
      $transition = $license->getState()->getWorkflow()->getTransition('activate');
      $license->getState()->applyTransition($transition);
      $license->save();

      $transition = $license->getState()->getWorkflow()->getTransition('confirm');
      $license->getState()->applyTransition($transition);
      $license->save();

So the activate transition is triggered first so in License presave method:

// Renewal completed.
        dpm($this->original->state->value);
        dpm($this->state->value);
        if ($this->original->state->value == 'renewal_in_progress') {
          $this->setExpiresTime($this->calculateExpirationTime($this->getExpiresTime()));
        }

$this->original->state->value is equal to pending and not renewal_in_progress.

And the other thing to handle is remoing item from the cart.

Grimreaper’s picture

Annnddd!!! Done.

Thanks for your help @joachim.

Renewing is ok. Removing an order item is handled.

Now it needs a (final) review and automated tests.

joachim’s picture

Status: Needs review » Needs work

Tests might have to wait, as the branch is failing: #2999402: tests are failing.

  1. +++ b/commerce_license.module
    @@ -72,9 +72,26 @@ function commerce_license_commerce_order_item_delete(EntityInterface $entity) {
    +    // We need the intermediate state of renewal_cancelled because otherwise in
    +    // the License entity preSave method we would continue to extend the
    +    // expiration of the license.
    

    Can't we just move it back to 'active'?

  2. +++ b/commerce_license.workflows.yml
    @@ -11,6 +11,10 @@ license_default:
    +    renewal_in_progress:
    

    I'd rather we call this 'renewing', to match order states.

  3. +++ b/commerce_license.workflows.yml
    @@ -11,6 +11,10 @@ license_default:
    +    renewal_cancelled:
    +      label: Renewal cancelled
    
    @@ -31,8 +35,16 @@ license_default:
    +    cancel_renewal:
    +      label: 'Cancel renewal'
    +      from: [renewal_in_progress]
    +      to: renewal_cancelled
    

    This should just go to 'active'.

  4. +++ b/commerce_license.workflows.yml
    @@ -31,8 +35,16 @@ license_default:
    +    renewal_in_progress:
    

    Urgh no sure what to call this.

  5. +++ b/src/EventSubscriber/LicenseOrderSyncSubscriber.php
    @@ -174,8 +184,17 @@ class LicenseOrderSyncSubscriber implements EventSubscriberInterface {
    +        $transition = $license->getState()->getWorkflow()->getTransition('confirm');
    

    That's the wrong transition -- the name doesn't make sense. We need a 'cancel renewal' transition.

  6. +++ b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
    @@ -0,0 +1,77 @@
    + * Handle license renewal.
    + *
    + * Set the already existing license in the order item.
    

    Comments for classes and methods should be in 3rd person, so 'Handles...' / 'Sets...'

Grimreaper’s picture

Here a patch that at least fixes the existing tests.

Point 6 from previous comment taken into account.

As seen together the problem is that we need the transition because we do not have more info in the presave method.

Also a query on order item referencing the license won't work because of this feature. We would have the order items from the previous orders that will reference the license.

Grimreaper’s picture

Tests that needs to be done:

  • For a non renewable license: you can't renew
  • For a renewable license:
    1. buy a license
    2. complete the order
    3. buy the same license in the renewable window
    4. complete the order
    5. see that the expires has been augmented of the license duration
  • For a renewable license: test that you can't renew outsite the renewable window
  • For a renewable license:
    1. buy a license
    2. complete the order
    3. buy the same license in the renewable window
    4. stop at the cart state
    5. remove the product from the cart
    6. see that the license in the active state and the expire time has been untouched
Grimreaper’s picture

@joachim,

I am currently in spectrum C (thunder sprint room). If we can meet to see how to do the tests, it would speed up closing this issue.

Grimreaper’s picture

Here are WIP tests.

We already found 3 bugs.

We need to figure out why the license is not updated.

Putting the list of tests to implement in the issue summary.

Status: Needs review » Needs work

The last submitted patch, 55: commerce_license-allow_renewal-2943888-55.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Grimreaper’s picture

Hello,

Here is a new patch that provides the remaining tests.

The tests implying cart manipulation still fail for an unknown reason. The order item is not the right one or not well detected.

I have updated the issue summary.

Grimreaper’s picture

Debugging the calls.

It is after the cart save that the order item is removed and it seems because of a bug in the existing license detection.

Because the existing license is in the "renewal_in_progress" state.

I continue testing and will provide a new patch by the end of the day of it is a success.

Grimreaper’s picture

I made the tests passing but after a last manual tests I found bugs.

When renewing a license the user lost is role.

The expiration time of the license is not updated correctly (but the tests about that are green...) Searching why.

Grimreaper’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
5.41 KB
39.71 KB

Ok.

I found out why it was not working anymore, it was because in my manual tests I used another order type than the default one.

I have added a message when trying to renew outside the renewal window for better UX.

@joachim: only stay the "problem" when in the renewal process the user lost the license grant. Is it a problem?

Thanks for the review.

Status: Needs review » Needs work

The last submitted patch, 60: commerce_license-allow_renewal-2943888-60.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Grimreaper’s picture

Status: Needs work » Needs review
FileSize
39.76 KB
885 bytes

Fix tests due to the addition of the drupal_set_message in License.php.

joachim’s picture

> @joachim: only stay the "problem" when in the renewal process the user lost the license grant. Is it a problem?

Doesn't sound good... can you explain more?
(And bear in mind I have forgotten EVERYTHING we discussed at DrupalEurope :) )

  1. +++ b/src/Entity/License.php
    @@ -476,4 +483,45 @@ class License extends ContentEntityBase implements LicenseInterface {
    +      drupal_set_message($this->t('You will be able to renew your license after %date.', [
    +        '%date' => \Drupal::service('date.formatter')->format($renewal_window_start_time->getTimestamp()),
    +      ]));
    +      return FALSE;
    

    We shouldn't set messages here, as this could be made by API calls.

    It's up to the caller (which knows what sort of context it's in) to set the message.

    Also, the message service should be used rather than dsm().

  2. +++ b/src/EventSubscriber/LicenseRenewalCartEventSubscriber.php
    @@ -0,0 +1,86 @@
    +    if ($existing_license && $existing_license->canRenew()) {
    

    ... here is where we should show the user a message along the lines of:

    'You have an existing existing license for this product [TODO: link here once there is user admin UI for licenses!]. This will be extended by PERIOD when you complete this order.'

Grimreaper’s picture

@joachim, Thanks for the review.

Here is a new patch where I moved the drupal_set_message where you asked.

Also I had chosen License.php canRenew method to know directly if the license was renewable and to get the license renewal window start.

About the lost of the license grant in the renewal process:

As soon as you add a product in your cart, if you already have a license that you can renew, this license is used in the order item and saved to have its state changed into "renewal in progress" state.

But in License.php::preSave(), has the state is changed and not set to "active" we fall into:

 // The state is being moved away from 'active'.
      if (isset($this->original) && $this->original->state->value == 'active') {
        // The license is revoked.
        $this->getTypePlugin()->revokeLicense($this);
      }

And so the grant (in my use case, the role) is removed.

joachim’s picture

Status: Needs review » Needs work

So you lose the grant during a renewal purchase? That's definitely a problem.

License.php::preSave() should know about the 'renewal in progress' state.

*sigh* This is getting SOOOO complicated :(

joachim’s picture

I'm almost wondering whether sticking the existing license on the order item at the point is too soon, and we should instead somehow mark the order item and only pick up the license at order completion.

Or add a 2nd ref field to the order item for 'renewing_license' and move the license from that to the normal license field at checkout.

Will ponder.

Grimreaper’s picture

@joachim: Thanks for your feedback. I will wait for your feedback on the best architecture for this feature.

joachim’s picture

Random thought I had yesterday... not had time to reread everything here, so could be crazy, feel free to point out flaws :)

We introduced 'renewal in progress' state because when an order is cancelled, we need to know which of these cases holds:

- this was a brand new license, and the license should be abandoned and cancelled
- this was a renewal, and the license should stay active

My thought was that to distinguish between these two states, we could instead do one of these:

- see whether older, closed orders refer to the license
- see whether the license's activation timestamp is older than the order being deleted

Grimreaper’s picture

Hi joachim,

Hum why not.

So with this solution, no need for the new state renewal in progress. OK.

I don't know when I will have the time to remake a patch.

drupgirl’s picture

Hi. To me this is the most valuable of all featured requests, as it falls into the most basic of use case: I have a role license, I want to be able to renew it before it expires so I continue to get all the fun features I bought without a day of missing out.

The patch is working for me: my user gets the state of "Renewal in Progress" with the correctly updated expires date. (Although I will be continuing to test this heavily throughout the day and night.)

This patch provides a great alternative to having to be forced to use Recurring in order to let users continue with their level of site access without a day of interrupted service, as they cannot add role licenses to carts when they already have that role. This module should be able to handle renewals without the aid of Recurring, imo.

Please help me to understand how to get this feature request into the module, as the module is somewhat unusable on its own without it due to the glitching of not being able to reup before expiration.

Thank you so much.

joachim’s picture

> Please help me to understand how to get this feature request into the module, as the module is somewhat unusable on its own without it due to the glitching of not being able to reup before expiration.

If you've got time to move this issue forward, that would be great!

Everything should be in the comments... collectively, they hold more information about this issue than I do, as it's been ages since I've worked on this module and I've forgotten everything. If things aren't clear, or there are gaps, post questions here and I'll try to remember!

calbasi’s picture

Hi,

Last patch don't just extend the time of the existent license, but create a new license. Then, after renewal, you have 2 licenses, one that is going to expire soon. And the second that will expire later (the new one).

But, when expiration date of the old license arrive:

- this license becomes expired AND its user receives a mail message warning about it (even when he has a second license, what is renewing thew old and now expired license).

- if the license is granting an user role, when the old license becomes expired, its user loose his role.

I think both are not designed behaviors and the second is, in fact, a major issue.

Kojo Unsui’s picture

Hi there,

I had a look at patch 64, made a bunch of testing, and AFAIK in my local version now, most of the renewable feature seem to be ok, with very few changes.

All of the listed tests above that need to be done are passed successfully.
I then changed messages as following :
- replaced all drupal_set_message with Drupal::messenger().
- added a "renewalStartTimeMessage" when the license is renewable but out of the renew dates window...
- in that same case, removed the NotPurchasableMessage (You may not purchase the @product-label product. )
- added a "You have an existing license for @product-label until @expires-time. This will be extended until @extended-date when you complete this order." if ($existing_license && $existing_license->canRenew())

Still to be done IMHO for renewable licenses :

- when a product variation type providing a license has « Activate license when order is placed » parameter set to true, expire time is not updated :
=> when licence added to cart, state changes to renewal in progress
=> when order in completed with payment, state changes back to active
=> but expire time is not updated & validating order afterwards does not update expire time neither

- add in the README that orders that contain a license have to be validated before renewing them, otherwise if you’re in the renewable window and purchase it again, this will create a duplicate instead
- or add a contrainst to prevent these dupes ?
- remove Drupal\commerce_order\Plugin\Validation\Constraint message @product-label is not available with a quantity of @quantity. (in fact this one is not specific to renewable licenses)
Thank you.

Kojo Unsui’s picture

Status: Needs work » Needs review
FileSize
51.22 KB

Here's a patch, based on @Grimreaper #64 one.

Kojo Unsui’s picture

Fix CartManagerTestTrait fatal error : it has been moved to Drupal\Tests\commerce_cart\Traits\CartManagerTestTrait.
(Todo it is deprecated, not sure how to rewrite the test)
Fix (hopefully) most coding standards messages.

Kojo Unsui’s picture

My bad, I forgot a couple of CartManagerTestTrait wrong use declarations. Provides a new one.

Status: Needs review » Needs work

The last submitted patch, 76: 2943888-commerce_license-allow_renewal-76.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Kojo Unsui’s picture

Status: Needs work » Needs review

Looked at the tests results :
- some report a problem similar to this Commerce Stripe issue about :

Commerce 2.15 introduced a new submodule commerce_number_pattern. If this is not correctly setup in the tests they will fail with errors such as Drupal\Core\Config\Schema\SchemaIncompleteException: No schema for commerce_number_pattern.commerce_number_pattern.order_default

- other failed tests refer to an existing issue : Tests broken for 8.x-2.x.

I may not pollute the current issue with these. The provided patch IMHO fixes most of current "renewable license" issue and I hope will be tested / reviewed soon.

When testing the feature, please remember that : orders that contain a license have to be validated before renewing its license, otherwise if you’re in the renewable window and purchase it again, this will create a duplicate.

Kojo Unsui’s picture

My version now ships also with following patches.

After some more days developing and testing, I can confirm that all tests listed are passed, with @grimreaper work and patch 76 above.

So I'm going to stick with this for now and push to production.

pmagunia’s picture

Wow Thanks for the patch Kojo.

I can confirm its working in my local dev environment.

It did not create duplicate licenses with my own limited testing.

The patch applied cleanly to the latest dev version.

pmagunia’s picture

I'm not sure this is correct but I think this updated patch solves all of the testing problems except one. Most of them were related to missing modules.

I wasn't sure how to fix the empty array but I thought I would throw what I have out there to get the ball rolling.

Status: Needs review » Needs work

The last submitted patch, 81: commerce_license-allow_renewal-2943888-81.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

pmagunia’s picture

I forgot to include some untracked files in the last patch.

pmagunia’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 83: commerce_license-allow_renewal-2943888-83.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

pmagunia’s picture

Status: Needs work » Needs review
FileSize
80.98 KB
4.26 KB

The test needed to bypass the access check to query storage.

Kojo Unsui’s picture

Status: Needs review » Reviewed & tested by the community

Wow (my turn ;-) thanks for handling the tests failure, Pmagunia !

I tested on a fresh install of demo project, plus License 2.x-dev and #86 applies smoothy.

I performed a few tests :

- Set up product, variations, orders and all the stuff...

- Purchased normal product, fine. Purchased non renewable license, fine. Purchased renewable license, fine.

- Finally re-purchased the same one in the renewable window. Message informing the license will be renewed is displayed, and the license is extended. I tried with Interval based on reference date then with rolling interval. I tried both with admin and anonymous accounts.

IMHO, RTBC.

Drupal Centric’s picture

Great work. I've tested on a product with a renewable license and rolling interval and all it worked fine. Renewal was allowed and interval extended correctly.

core: "8.9.1"
commerce: "2.x-dev",
commerce_cart_advanced: "^1.0@beta",
commerce_cart_flyout: "^1.7",
commerce_cart_redirection: "^2.1",
commerce_license: "2.x-dev",
commerce_paypal: "1.x-dev",
commerce_reports: "1.x-dev",
commerce_shipping: "2.x-dev"

Joao Sausen’s picture

I'm using the patch on #86 on a project and its working fine. Thanks!

fy1128’s picture

How to avoid the queue job that was created before expiring the license that had been renewed?

fy1128’s picture

Patched the AdvancedQueue plugin 'commerce_license_expire'.

to do not expire the license that was renewed before the queue being processed.

if ($license->getExpiresTime() > $this->time->getRequestTime()) {
      return JobResult::failure('License is not expired.');
}

And I think the expire time should be re-calculated from now if it was already expired.

fy1128’s picture

FileSize
8.99 KB
ChristopheDG’s picture

Any time estimate when this would be committed to the dev version?

I'm starting a new project where this module/feature would be an excellent fit...

Thank you for all the hard work everyone!

lubwn’s picture

I need the very same functionality but with unlimited licenses. Because I am using Commerce Subscription and it requires to set the license type to unlimited, since intervals are managed by subscriptions instead of license itself (as far as I understand).
Patch applied smoothly but did nothing to my installation, people still can not upgrade their subscription / renew license because since it is unlimited it just says they may not pursache the same license again.

Any solution to this? Thanks in advance!

arunkumark’s picture

I have resolved the issue by using the hook_entity_insert() in my Custom module. Below is the hook implementation.


/**
 * Implements hook_entity_insert().
 * 
 * License expires recalculate for an existing license. 
 */
function MODULE_entity_insert(Drupal\Core\Entity\EntityInterface $entity) {
  if ($entity->getEntityType()->id() == 'commerce_license') {
    $uid = $entity->getCurrentUserId();

    // Check license type already purchased by the same user or not.
    $plugin_id = $entity->getExpirationPluginType();
    $request_time = \Drupal::service('datetime.time')->getRequestTime();

    $expiration_type_plugin = $entity->getExpirationPluginType();
    $query = \Drupal::service('entity_type.manager')->getStorage('commerce_license')
      ->getQuery()
      ->condition('state', 'active')
      ->condition('expiration_type__target_plugin_id', $plugin_id)
      ->condition('uid', $uid)
      ->condition('type', $entity->type->value)
      ->condition('expires', $request_time, '>=')
      ->condition('expires', 0, '<>')
      ->sort('license_id', 'DESC')
      ->range(0, 1);
    $license_ids = $query->execute();
    
    foreach ($license_ids as $license_id) {
      $expiration_plugin = $entity->getExpirationPlugin(); // Plugin type.
      $oldLicense = Drupal\commerce_license\Entity\License::load($license_id);
      $request_time = $oldLicense->getExpiresTime();

      // Get similar licenses.
      $start_date = (new \DateTimeImmutable('@' . $request_time))
        ->setTimezone(new \DateTimeZone(commerce_licence_get_user_timezone($entity->getOwner())));
      $expiration_date = $expiration_plugin->calculateDate($start_date);
  
      // The returned date is either \DateTimeImmutable or
      // \Drupal\recurring_period\Plugin\RecurringPeriod\RecurringPeriodInterface::UNLIMITED.
      if (is_object($expiration_date)) {
        $expiration_date = $expiration_date->format('U');
      }
      
      $entity->setExpiresTime($expiration_date);
      $entity->save();
    }
  }
}
robcarr’s picture

Has this still not made it to DEV? The patch at #91 won't apply (via Composer) to the current DEV release (11 Feb 21)

Kojo Unsui’s picture

@robcarr, have you tried #86? So far it was applying smoothly to DEV.

joachim’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/src/Form/LicenseCheckoutForm.php
    @@ -57,17 +57,23 @@ class LicenseCheckoutForm extends ContentEntityForm {
    -        drupal_set_message($this->t('Saved the %label License.', [
    

    Same here.

    Please don't do unrelated fixes in a patch. It makes it MUCH harder to review.

    Even if they're necessary, do them in a separate issue that you can reference as needing to be committed first.

  2. +++ b/tests/src/Kernel/CommerceOrderSyncTest.php
    @@ -4,7 +4,7 @@ namespace Drupal\Tests\commerce_license\Kernel;
    -use Drupal\Tests\commerce_cart\Kernel\CartManagerTestTrait;
    

    These fixes to tests were maybe committed recently on a separate issue and hence no longer apply?

    If they are still needed, they should be moved to a separate issue anyway, as they add noise to this patch.

robcarr’s picture

Also tried the patch at #86 and that fails too 🤔

robcarr’s picture

I've just tried to unpick the patches at #86 and #91, and manually apply the text changes (taken most of the day). The patch files are long (obviously lots of work) and many changes are just unrelated code formatting changes to the whole module - which is an unhelpful distraction to trying to solve this significant issue.

However, I lose track about half way through patch #91 with the changes in src/LicenseAvailabilityCheckerExistingRights.php with the new function introduced:

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()) {
      return AvailabilityResult::neutral();
    }

    // Show a message that includes the reason from the rights check.
    if ($user->id() == $this->currentUser->id()) {
      $rights_check_message = $existing_rights_result->getOwnerUserMessage();
    }
    else {
      $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(),
    ]);

    return AvailabilityResult::unavailable($message);
  }

How is $unsetNotPurchasableMessage dealt with?

Also could not make sense of the patches on quite a few of the tests which have clearly changed a lot and most of the changes are already in DEV.

I haven't tested the patch particularly thoroughly, but it seems functional and should apply to the current DEV release (and prob alpha20 too). I'd be grateful if anyone with a bit more expertise could review it, especially the tests and the changes to src/LicenseAvailabilityCheckerExistingRights.php

robcarr’s picture

Sorry last patch had loads of trailing white space errors so wouldn't apply. One attached to this post should apply and work, but if anyone can review the tests listed in the patch at #91, plus the private function checkPurchasable() function in src/LicenseAvailabilityCheckerExistingRights.php in this patch then that may push this issue closer to a solution.

robcarr’s picture

Although the patch at #101 applies locally, it won't apply via Composer and d.o. plus the test has come back with loads of errors. I'm probably a little out of my depth here... 🆘

robcarr’s picture

robcarr’s picture

I'm making lots of mistakes (aka learning) here... attached is yet another patch file generated by a different tool and seems to include full path to module from webroot, so it may not apply to a vanilla D8/9 installation. My previous patches didn't include the 3 new pages created, so they're in this one.

robcarr’s picture

The patch at #104 (commerce-license-ability-to-re-purchase-a-license-to-extend-it-102.patch) just fails everywhere. I hope it's of some use to someone else who can take this forward.

joachim’s picture

diff --git web/modules/contrib/commerce_license/commerce_license.module web/modules/contrib/commerce_license/commerce_license.module

Well your first problem is that you've made the patch from the wrong place!

If your whole site codebase is in git, then do:

1. cd web/modules/contrib/commerce_license
2. git diff --relative -- .

That gets you a git diff of just the current folder, with the file paths in the diff relative to where you are.

robcarr’s picture

The other problem is that the code in my patches just don't work. Without having much knowledge of the Commerce framework, I tried my best to manually map across a lot of the code from the older patches; clearly my lack of expertise failed in this area.

The renewal functionality is really important to make this module more relevant to real world solutions. I'm also looking at more flexible ways to send expiry notifications (eg, 30 days before expiry), but such functionality depends on this issue being addressed.

robcarr’s picture

Latest updated patch. Tried this out on a few scenarios, so appears functional. However, needs work on tests.

robcarr’s picture

Previous patch failed to include new files 🙄

robcarr’s picture

Added back trait LicenseOrderCompletionTestTrait (/tests/src/Kernel/LicenseOrderCompletionTestTrait.php) to try and get the patch through some tests. This trait was removed in alpha19 (probably for good reason), but causes a critical failure if not included.

robcarr’s picture

The patch at #110 fails a few tests.

If a license is being renewed, but during checkout the item is deleted from the cart, then the whole user's license is deleted. This is current erroneous behaviour on this patch (tested manually too) and reason test fails.

Unsure of reasons for failures in 2-4, one of which being that the base table commerce_license doesn't exist...

joachim’s picture

> If a license is being renewed, but during checkout the item is deleted from the cart, then the whole user's license is deleted. This is current erroneous behaviour on this patch (tested manually too) and reason test fails.

This was a REALLY HARD corner case to figure out.

When the product is in the cart, we need to know whether it's first-time purchase or a renewal purchase, basically, because how to restore things when the user removes the item from the cart is completely different in those two cases.

robcarr’s picture

Now need to focus on /src/EventSubscriber/LicenseRenewalCartEventSubscriber.php and add public function onCartOrderItemRemove(CartOrderItemRemoveEvent $event). Thought this might work:

public static function getSubscribedEvents() {

    $events = [
      CartEvents::CART_ORDER_ITEM_REMOVE => ['onCartOrderItemRemove', 10],
      CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', 0],
    ];
    return $events;
  }

I can't get that to trigger a public function onCartOrderItemRemove(CartOrderItemRemoveEvent $event), so not sure where else to look to catch cart item deletions. 🤔

junaidpv’s picture

I improved #110.

CommerceOrderSyncRenewalTest was failing because the "activate_on_place" setting was not enabled so not extending the expiry. I added a line to enable "activate_on_place" for the product variation which fixed it.

Regarding removing the item from the cart. There is already commerce_license_commerce_order_item_delete() to delete the license unconditionally. I improved that part. If the license is in the renewal_in_progress state, then it will first put the license in the renewal_cancelled state then back to the active state. We can't put the license in active state directly because of the way License::presave() acts if license transits from renewal_in_progress to active state by extending the expiry time.

I could not fully solve failing of CommerceAvailabilityExistingRightsTest. Need to fix that.

IMPORTANT: The patch is incompatible with the latest release, 8.x-2.0alpha21. It always create new license when attempting to renew the existing one. I think it is happening because of the change appeared in this commit.

junaidpv’s picture

Status: Needs work » Needs review
FileSize
49.89 KB

Today I found I was wrong with my statement yesterday. It works with 8.x-2.0-alpha21 too. Improved CommerceAvailabilityExistingRightsTest by adding to install the schema for commerce_license entity, which fixed the error happened #110.

robcarr’s picture

Thank you so much for your work on the patch Junaid. I've tested it and it works really well, including the case if the license is removed from the cart. Fantastic progress.

Would be really helpful if someone else could review and double check.

+1 for RTBC

junaidpv’s picture

Status: Needs review » Needs work

@robcarr glad to hear you found it working.

However, we found it having a con which may require more changing.

We have a production having multiple variations allowing to purchase roles license for 1 year, 2 year and 3 year.

On further testing, we found it is not allowing to repurchase the role license with a different product variation of the same product.

For example, Let's assume I purchased 1 year membership before. Then I am only able to re-purchase 1 year member, not 2 or 3 year memberships!

@all Any inputs and or improvement on the patch are highly appreciated.

Drupal Centric’s picture

Thanks @junaidpv that's working fine for me too.

+1 for RTBC

robcarr’s picture

The only workaround for your patch is to let license expire, which would allow a license of any duration to be re-purchased. Not ideal, but would work. I'm tempted in my current project to only offer 12 month licenses to be purchased.

robcarr’s picture

The patch at #115 no longer works with latest DEV (6 Aug 21)

robcarr’s picture

Re-roll attached. Seems to work, but needs a bit more testing.

Still has the issue where users can only renew for the same duration as currently held.

joachim’s picture

> Still has the issue where users can only renew for the same duration as currently held.

I really don't know how that can be changed.

The license is tied to a product variation, which sets the duration.

We'd have to rethink how the renewal works -- let the user pick a different PV, but still enforce that it has to be for the same grantable. Complex stuff.

robcarr’s picture

Status: Needs work » Needs review

I think we could quickly get into diminishing returns to allow users to renew a different license (Product Variation). It looks incredibly complex.

If the patch provides acceptable functionality (and a few others can review it), then this might be the most pragmatic way to allow users to renew, as it fulfils the majority of use cases. The caveats would be:

  1. Users can only renew their existing license for the same duration, eg, renew a 12 month license for another 12 months.
  2. If users wish to move to a different license plan (PV), they have to wait until their current license has expired

The 2nd caveat is the same functionality as offered by the Commerce License module anyway (ie, the license has to expire before renewal is possible), but this patch at least permits renewal prior to expiry in the majority of cases.

robcarr’s picture

Status: Needs review » Needs work

The last submitted patch, 124: commerce_license-allow-renewal-2943888-124.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

robcarr’s picture

Patch #124 seems to be failing testing of Drupal\Tests\commerce_license\Kernel\CommerceOrderSyncRenewalTest::testRemovingProductFromCart at line 390:

   // Remove the item from the cart.
    $this->cartManager->removeOrderItem($cart_order, $order_item);

Resulting in 2 errors:

  • Drupal\Core\Entity\EntityStorageException: Attempted to save order 1 with version 2. Current version is 3.
  • Drupal\Tests\commerce_license\Kernel\CommerceOrderSyncRenewalTest: test runner returned a non-zero

With no experience of automated testing, I've spent a day scratching my head and getting nowhere with the test code. With a manual test of the 'failing' function (ie, deleting a renewing license from the basket) it works fine, so it seems there's something wrong with the test, rather the patched module code itself.

joachim’s picture

+++ b/commerce_license.module
@@ -73,7 +73,24 @@
+    // So, first put it into renewal_cancelled state, then to active.

That looks REALLY hacky.

junaidpv’s picture

@joachim Agree. But that was only way I found as said in #114.

SocialNicheGuru’s picture

caused major error:

Drupal\Core\Entity\EntityStorageException: $string ("You already have one item similar to @product-label in your cart.") must be a string. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 846 of drupal8.9/html/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

I upgrade the commerce_license
I goto a product page like product/3 and I get WSOD with the above in my logs

pmagunia’s picture

Re-roll from #115.

In PHPUnit 9, arraySubsetAsserts is removed. I added a dev package to composer.json which replaces that function.

@robcarr the version error in the test you were seeing was because the cart needed to be reloaded.

Is the inability to renew a license with a different variation a blocker?

EDIT: Didn't mean to add the same file 2x. They are identical.

pmagunia’s picture

pmagunia’s picture

Status: Needs work » Needs review
FileSize
51.87 KB

Now with all Drupal tests.

robcarr’s picture

@pmagunia thank you for your work on an updated patch. I'm currently working an a different project and I've had to park the project utilising Commerce/Commerce License, but will hopefully test again soon.

I don't believe that the the inability to renew a license with a different variation is a blocker. From a usability perspective, a user wishing to renew and shift to a different plan would just have to let the current plan (License) expire. I think the patch's implementation would suit the majority of cases. It would better to commit this patch to the module and raise a separate issue if this become significant to others.

joekers’s picture

Thanks for the updated patch pmagunia, I've given it a test and it seems to be working well!

I was getting the same issue as SocialNicheGuru with the previous patch I was using (#124) and also the latest patch.
Drupal\Core\Entity\EntityStorageException: $string ("You already have one item similar to @product-label in your cart.") must be a string. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 846 of drupal8.9/html/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

I found that it's caused by this line and is caused by $message already being TranslatableMarkup:
\Drupal::messenger()->addError(t($message));

I've updated it to this in the patch - it's the only change from #132:
\Drupal::messenger()->addError($message);

joekers’s picture

I'm not sure if we should be covering the case where a user tries to add the same product to their cart when the license is already "renewal_in_progress". I tried this and got some undesired behaviour.

The product was added to the cart and the quantity was increased even though LicenseMultiplesCartEventSubscriber is supposed to force the quantity. There weren't any messages about the item being added to the cart either.

The reason for this is due to the LicenseRenewalCartEventSubscriber trying to apply the "renewal_in_progress" transition again to the license. This causes an exception in the Renderer and skips the other event subscribers:
The transition "renewal_in_progress" is currently not allowed. (Current state: "renewal_in_progress".)

Therefore I've added a check to make sure that the license isn't already in the state before applying it. This allows the process to continue as normal. I've done some manual testing and this seems to be working well.

joekers’s picture

One loophole that I've found is that if the user is logged out, adds the product to the basket, and then logs in within the checkout flow. This creates a new basket and the event subscriber checks are bypassed.

I'm not sure if this is the right approach, but is there any way we could check the contents of the basket when it's assigned to the user on login?

robotjox’s picture

Patch fails to apply for me on Drupal 9.3.6

robotjox’s picture

Rerolling as it did not apply on 9.3.6.

junaidpv’s picture

@robotjox, It appears, you added previous patch file in your patch :) Please correct it. Thanks!

robotjox’s picture

oops, sorry about that! Will try to get a new patch up today

robotjox’s picture

joekers’s picture

Another thing I've noticed is that the license is revoked when the license is being renewed. I don't think this is desirable as if the user decides to leave the checkout without renewing, then their license will remain revoked.

I think we just need to update the condition around when a license is being changed from the 'active' state to also check the renewal state before revoking the license. I can update the patch when I get a chance.

robcarr’s picture

We'd looked at that in #114. It's an important scenario to catch, as abandoned carts are quite a frequent occurrence. Thanks for all your work @joekers

attisan’s picture

The changes to the LicenseOrderProcessorMultiples result in breaking carts with multiple products with (potentially different) licenses being added to a cart - so only a single product with a license can be purchased at a time. This specifically breaks the usage of pado or vado where multiple licenses (e.g. roles) should be bought together.

attisan’s picture

Moved the latest patch (#141) to the issue fork, hid the tremendous amount of patch files and changed LicenseOrderProcessorMultiples to allow multiple different licenses in a single order.

Took a look at the test issues and figured to leave it be as long as they seem to stem from the dev branch.

Would be great if we could continue working on the issue fork (with the open MR). Patch can be applied just like it is possible with patch files using the plain diff link.

junaidpv’s picture

Applies fixes for:

  • issue which revoke license when product is added to cart. So, abandoned cart wont be a problem now.
  • issue which incorrectly sets the renewal time when product is removed from cart.

@joekers, @robcarr . Please have a look and try to see if it works for you as well.

junaidpv’s picture

As already stated earlier, it is not possible to repurchase a different variation. Fixing that will require a major rewrite of the module.

We wanted to fix that in our project. I prepared a patch specifically for our project. We have only three PVs granting role for 1, 2 and 3 years. This hack helped to work with them. I guess, it wont work if you are having more than one type of PVs and more attributes.

Please replace PV bundle name in this patch "our_project_membership_pv" with the corresponding PV bundle name in your project.

Looking forward to hear if anybody found this helpful and feedback.

jsacksick’s picture

Issue tags: +KickstartPrague2022
rszrama’s picture

Title: ability to re-purchase a license to extend it » Add the ability to re-purchase a license to extend it

  • 394a3a7 committed on 8.x-2.x
    Issue #2943888 by pmagunia, Grimreaper, robcarr, junaidpv, Kojo Unsui,...
TomTech’s picture

Assigned: Unassigned » TomTech
Status: Needs review » Fixed

This (large) patch/MR (with some modifications) has been committed.

The primary additional changes are:

  1. Better handling during delete of an order item
  2. Handle more cases were renewal_in_progress should be checked alongside active, e.g. during the deletion of a license or when cron is processing expiring licenses
  3. Fix/Update more tests

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Morbus Iff’s picture

This patch appears to be partly responsible for critical error in 3.0-rc1: https://www.drupal.org/project/commerce_license/issues/3362663