Do not delete all order items from the current order when cancelling a subscription

Created on 3 May 2023, over 2 years ago

Problem/Motivation

After subscription cancellation, I'd like to do something based on the order that was cancelled, in its most recent state. That's the current (draft) order.

But when cancelling a subscription, RecurringOrderManager::applyCharges() removes all order items from that order. It does so because there are now no charges to apply to the order, and it removes the "unused" order items.

Valuable followup data is being lost here.

Steps to reproduce

Cancel a subscription, check the current order (now canceled). There are no order items and no order amount.

Proposed resolution

Any solution might need to be configurable behaviour, some implementations may expect the order to be empty after subscription cancel?

As a workaround, I'll likely store the order items at subscription pre-cancel and restore them as soon as possible after that.

πŸ› Bug report
Status

Active

Version

1.0

Component

Code

Created by

πŸ‡³πŸ‡ΏNew Zealand john pitcairn

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @john pitcairn
  • πŸ‡ͺπŸ‡ΈSpain qtarant

    Hello. I have exactly the same issue. As a collateral effect subscriptions are never canceled, because the "draft" order is canceled because there is no order items on order, and after that cron is searching draft orders and the order (related with a canceled subscription) was canceled so cron not applies changes on subscription state.
    I fixed checking if there are charges before create new order items and delete previous order items on the order.

  • πŸ‡¬πŸ‡§United Kingdom code-brighton

    Hi qtarant, can you post the code you changed? Or supply a patch? Many thanks

  • πŸ‡ΊπŸ‡ΈUnited States sapiusenator

    I don't know if this is 100% or not, but worth a gander I suppose.

    namespace Drupal\my_custom
    
    use Drupal\commerce_recurring\RecurringOrderManager as BaseRecurringOrderManager;
    use Drupal\commerce_order\Entity\OrderInterface;
    use Drupal\commerce_recurring\Entity\SubscriptionInterface;
    use Drupal\commerce_recurring\BillingPeriod;
    
    /**
     * Extends RecurringOrderManager to preserve order items when subscriptions are canceled.
     * 
     * The default behavior deletes all order items when a canceled subscription has no charges.
     * This override preserves those items by skipping the deletion process and transitioning
     * draft/needs_payment orders to canceled state instead.
     */
    class MyCustomRecurringOrderManager extends BaseRecurringOrderManager {
    
      /**
       * {@inheritdoc}
       */
      protected function applyCharges(OrderInterface $order, SubscriptionInterface $subscription, BillingPeriod $billing_period, BillingPeriod $trial_period = NULL) {
        // Collect charges to see what we're dealing with
        $charges = [];
        if ($subscription->getState()->getId() == 'trial') {
          $charges = $subscription->getType()->collectTrialCharges($subscription, $billing_period);
        }
        else {
          $charges = $subscription->getType()->collectCharges($subscription, $billing_period);
        }
    
        // If subscription is canceled and there are no charges, preserve items
        if (empty($charges) && $subscription->getState()->getId() === 'canceled') {
          // Check if order should be canceled
          $order_state = $order->getState()->getId();
          if (in_array($order_state, ['draft', 'needs_payment']) && $order->hasItems()) {
            // Apply cancel transition
            $transitions = $order->getState()->getTransitions();
            if (isset($transitions['cancel'])) {
              $order->getState()->applyTransitionById('cancel');
            }
          }
          
          // Skip processing - this preserves the items
          return;
        }
    
        // For all other cases, use the parent's normal logic
        parent::applyCharges($order, $subscription, $billing_period, $trial_period);
      }
    }
Production build 0.71.5 2024