Currently a subscription that fails to process payments (using retries/extensions) is marked 'UC_RECURRING_FEE_STATUS_EXPIRED'. The same thing happens when a subscription runs its full course and completes successfully after the last payment has been made. There is no way to differentiate them after it has happened.

These scenarios are not the same and should not be treated the same way. Even though we have lenient retry attempts, 10 days total to update CC info, with emails sent on each failed attempt, we frequently get customers who wish to update their credit card info and restart their subscription after it has been marked expired.

This causes a number of problems:

  • Remaining intervals is set to 0.
  • Customers cannot update CC info because the recurring fee is no longer visible to them.
  • Admins cannot differentiate between failed due to payment vs. subscription ended term.
  • Same hooks are used both scenarios which means customers are removed from functionality when it would be better to temporarily suspend them and have them resume where they left off when successful payment is made.

Looking through the code I see UC_RECURRING_FEE_STATUS_SUSPENDED which does not appear to be used anywhere. I'm thinking that this should be used when subscription is canceled due to failed payments and adding a new CA trigger and module hook so that it would be easier to code for this situation. Would like to know what others think?

Comments

tinker’s picture

Version: 6.x-2.x-dev » 7.x-2.x-dev
Assigned: Unassigned » tinker
Status: Active » Needs review
StatusFileSize
new13.83 KB

Functionality changes:

  • User who fails payment on all retry attempts is put into suspended state.
    • Suspended fee are not processed on CRON runs.
    • Fee remaining intervals is not set to zero.
  • Customer can now see suspended fee in My account -> Recurring fees.
  • Customer can use the 'Update' fee operation link (if provided by payment processor) to update their billing info. If info update is successful then the fee becomes active again.

This means that intervention from admins is no longer required to resume fees that failed payment.

Added 3 new hooks and matching rules event triggers:

  • hook_recurring_renewal_suspended (rules uc_recurring_renewal_suspended) - triggered when fee renewal suspended by admin.
  • hook_recurring_renewal_suspended_failed (rules uc_recurring_renewal_suspended_failed) - triggered when fee renewal suspended due to failed payments.
  • hook_recurring_renewal_resumed (rules uc_recurring_renewal_resumed) - triggered when admin sets fee to active (when not before) OR when customer updates billing info successfully.
  • Additional order messages when fee is suspended or resumed.

Code format changes:

  • Variable concatenation
  • Reduce length of code by using $fsv as replacement for $form_state['values']
  • Change admin edit form to use $fee_edit object rather than random variables which allows for easy comparison to original $fee object and quick save of changes using drupal_write_record. This also fixes a bug where undefined variables where being used.
  • Use defined UC_RECURRING_FEE_STATUS_... instead of integers

To Do

  • The new 'suspended due to failed payment' hook can be used directly to send out the payment failed all retry attempts email. The is no Rules action for this in current dev so this should be added. Rules port going on in #1042262: Port uc_recurring to Rules instead of CA
  • Conditional Actions could also be changed if they don't go the way of the Dodo. The CA triggers have been commented out so I did not update the CAs.
  • It's up to the payment processor modules to set a fee to active when payment info is sucessfully updated. I have only made this change to the uc_recurring Authorize.net CIM quasi module.

Patch is for 7.x-2.x-dev and can be applied using 'git am [patch file]'

tinker’s picture

Title: Subscription expired due to failed payments is treated the same as sub expired that completed successfully » Set fees that fail payment attempts to suspended status, do not expire, allow resume, new hooks.

Changing title to better describe the changes.
Attached patch contains small change in uc_recurring.admin.inc: Use uc_recurring_fee_user_save() instead of drupal_write_record() so that fee call be altered and hook_recurring_fee_user_saved is invoked.

Patch can be applied using git am uc_recurring-failed-payment-to-suspended-2020337-2.patch

tinker’s picture

Forgot to attach patch.

tinker’s picture

Back port uc_recurring-failed-payment-to-suspended-2020337-2.patch to 6.x-2.x-dev

tinker’s picture

tinker’s picture

Issue summary: View changes

Updated issue summary.

stewart.adam’s picture

Rebased patch for 7.x above against latest dev (just a change in the array structure of Rules event declaration).

glynster’s picture

I just came across this and wanted to know if this has been used in production? This patch has never been committed either? Any reasons as I see this updated to be invaluable!

tinker’s picture

@glynster, Yes this has been used in production D6 site ever since I posted the patch. Please note that it also may require #2030267: Fee operation links access control faulty, wrong arguments on admin fees overview page to ensure proper permissions for operation links to suspend/resume recurring fees and update payment info.

Not included in this patch is additional changes used to calculate next interval timestamp. We had an issue where a user would suspend a fee for a few months and then resume. This would cause the fee to catch up to where it left off. e.g. if it was suspended for three months then 3 payments would be made in one day to catch up. Using the patch below one payment would be made instantly when resumed and the next would be plus one regular interval (e.g. one month) from current date.

function uc_recurring_next_interval_timestamp($fee) {
// original code

  // Additional check for resuming fees that have been suspended for a while.
  if ($next_timestamp < REQUEST_TIME) {
    global $user;
    $next_timestamp = strtotime('+' . $fee->regular_interval, REQUEST_TIME);
    $message = t('Recurring Payments:') . '<br>';
    $message .= t('Next interval already in the past which would cause multiple'
      . ' billing. Moving next interval to @date', array('@date' => format_date($next_timestamp, 'large')));
    uc_order_comment_save($fee->order_id, $user->uid, $message);
  }
  return $next_timestamp;
}

I was planning on adding this as a configuration setting as both methods may be valid depending on business logic.

glynster’s picture

@tinker thanks so much for getting back to me so fast, makes such a difference!

So a quick recap to make sure I have understood everything:

1. Apply the patch #6 for us as we are on Drupal 7.
2. Add the above code from #8 to a custom module to accommodate the resuming fees.
3. Apply the Patch #4 to fix admin links etc.
4. As we are using Authorize.net CIM your patch has covered that as well, correct?
5. We also added a new rule which updated the order state for these occurrences, eg. if a subscription fails or expires the order is updated to the state of Failed, rather than leaving all those orders in a Pending mode. On our end this became very confusing as Pending is used for many things. Much easier to identify now.

Are you a maintainer of this module? Do you know how we can have this code committed and a new release added? This module is extremely valuable, along with your expertise.

glynster’s picture

@tinker I am trying to get some traction on having your patch as well as your above time-stamp tweak and some other possible patches committed. I have gone ahead and contacted a maintainer but not luck hearing back from them. This is what we have managed to do so far: https://www.drupal.org/node/1247770 Any help or guidance would be great.

tinker’s picture

@glynster, For patches to get accepted they must be reviewed by multiple people and then they can be set 'Reviewed & tested by the community' (RTBC). The problem is that no one is reviewing them. At some point (after years) univate probably gives up on anyone reviewing outstanding patches and take the time to do it himself. This is a huge undertaking. So if you want to help go through the issue queue and try all the patches that 'Need review' and report if they work or not. If you are the 3rd or 4th person that says it works you can change the status of the issue to RTBC.

glynster’s picture

@tinker, can you please confirm my steps in comment #9 so then I can add this to our production site. Then I can say the commit is successful and should be committed.

Thank you for your time.

tinker’s picture

@glynster, Each patch is for a different issue and has to be reviewed individually. This one is complex because it exposes other issues. You should start with #2030267: Fee operation links access control faulty, wrong arguments on admin fees overview page #4 and say if that fixes the fee operation links. Then do the patch #6 from this issue and say if it allows suspending and resuming fees.

Finally the issue mentioned in #8 is not a bug but a feature. It should probably have a new issue created. Some people may want it to operate so that the customer has to pay for all intervals that they missed when they resume. Others will want the customer to only pay one interval when they resume. My current working version is for Drupal 6 and is very heavily customized. The code in #8 is wrong. If you want the later you would replace uc_recurring code with something like (untested):

function uc_recurring_set_intervals(&$fee) {
  $next_charge = strtotime('+' . $fee->regular_interval, $fee->next_charge);
  if (!empty($fee->data['extension'])) {
    $next_charge -= $fee->data['extension'];
  }
  
  // Check if next interval is in the past, due to suspended fee, and move it
  // into the future so multiple billings will not immediately occur.
  $skipped_intervals = 0;
  while ($next_charge < REQUEST_TIME) {
    $next_charge = strtotime('+' . $fee->regular_interval, $next_charge);
    $skipped_intervals++;
  }
  if ($skipped_intervals) {
    global $user;
    $message = t('Recurring Payments:') . '<br>';
    $message .= t('Next interval already in the past which would cause multiple'
      . ' billing. Moving next interval to @date', array(
      '@date' => format_date($next_charge, 'large')
    ));
    uc_order_comment_save($fee->order_id, $user->uid, $message);
  }

  $fee->next_charge = $next_charge;
  $fee->data['extension'] = 0;
  $fee->attempts = 0;
  
  if ($fee->remaining_intervals > 0) {
    $fee->remaining_intervals--;
  }
  $fee->charged_intervals++;
}
As we are using Authorize.net CIM your patch has covered that as well, correct?

Nothing changes on the payment side so it should continue to work.
Sorry spoke too soon. The payment handler re-activates the fee if payment is successful. This is the only change. You are covered.

We also added a new rule which updated the order state for these occurrences, eg. if a subscription fails or expires the order is updated to the state of Failed...

OK, whatever fits your work flow.

glynster’s picture

@tinker you are so helpful and a great asset to the Drupal community. I have commented on #2030267: Fee operation links access control faulty, wrong arguments on admin fees overview page, works well for me however I left a question in relation to Order Status. Once I hear back then I can move onto the next patch.