Jump to content

Using OrderProductUpgradeOverride hook to correct discounted service upgrade values

Recommended Posts

When placing pro-rated upgrade order that also has a recurring discount, the calculated price is incorrect. The formula being used is:

Old Product/Service

Price Per Day * Number of days until next due date = Amount Credited


New Product/Service

Price Per Day * Number of days until next due date = Amount Debited


Total Payable Today = Amount Debited - Amount Credited


Discounted Upgrade Price = Total Payable Today - Discount


This is fine for a one-off discount on the upgrade itself, but is incorrect for a pro-rated upgrade on the service where the discount should be applied to the Amount Debited. An older thread discussed the issue.



These values can be overridden when the user selects the product to upgrade to using the OrderProductUpgradeOverride hook. Here's an example of the variables passed to it:




[oldproductid] => 159

[oldproductname] => Old product

[newproductid] => 161

[newproductname] => New Product

[daysuntilrenewal] => 25

[totaldays] => 31

[newproductbillingcycle] => monthly

[price] => 6.52

[discount] => 1.63

[promoqualifies] => 1



price is the Total Payable Today from above. There's not much to go on here, but in our situation we have no absolute value discounts (just percentages). We can use the price and discount to get the discount percentage and from that calculate what the price and discount should be:

Discount Rate = discount / price


New Price = price / (1 + Discount Rate)


New Discount = New Price * Discount


This is far from perfect, price and discount have already been rounded, so any value derived from them will have a margin of error. Here's the hook code:


function myAddon_OrderProductUpgradeOverride ($vars) {
   $return = array();

   if ($vars['price'] == 0) {
       $discRate = 0;
   } else {
       $discRate = $vars['discount'] / $vars['price'];

   $return['price'] = round( $vars['price'] / ($discRate + 1) , 2);
   $return['discount'] = round( $return['price'] * $discRate, 2);

   logModuleCall('myAddon', 'OrderProductUpgradeOverride', '', $vars, $return, array());

   return $return;

add_hook('OrderProductUpgradeOverride', 1, 'myAddon_OrderProductUpgradeOverride');


If anyone can spot any problems or offer any improvements to the above, I'd be happy to hear them. I can get the new product information using the newproductid but no way to get the correct currency value. If the customer is ordering the upgrade we can get their currency, but if an admin orders it from within WHMCS it's not possible.


If the hook passed the promotion code and the price for the full payment period, the calculation could be more robust.

Link to comment
Share on other sites

  • 2 weeks later...

My original calculations were too naive. After some more experimentation it became clear that to calculate the correct value you need to know the following values:

  • Current service price
  • New Service Price
  • Days left on current cycle
  • Total days in current cycle
  • Total days in new Cycle
  • Discount rate


I now have a revised hook that appears to be working (with some rounding errors):


function myAddon_OrderProductUpgradeOverride ($vars) {
   $return = array();

   $pid = $vars['newproductid'];
   $daysLeft = $vars['daysuntilrenewal'];
   $oldTotalDays = $vars['totalDays'];
   $newCycleMonths = 1;
   $newBillingCycle = $vars['newproductbillingcycle'];
   switch($newBillingCycle) { // convert billing cycle to number of months
       case 'quarterly':
           $newCycleMonths = 3;
       case 'semiannually':
           $newCycleMonths = 6;
       case 'annually':
           $newCycleMonths = 12;
       case 'biennially':
           $newCycleMonths = 24;
       case 'triennially':
           $newCycleMonths = 36;
   $price = $vars['price'];
   $di****ount = $vars['discount'];
   $today = new DateTime();
   $newStart = new DateTime();
   $newStart->sub(new DateInterval('P'.$newCycleMonths.'M'));
   $DInewTotalDays = $newStart->diff($today);
   $newTotalDays = 1 + $DInewTotalDays->days; // get the number of days over the new billing cycle
   $return['newCycleMonths'] = $newCycleMonths;
   $return['newTotalDays'] = $newTotalDays;

   if (isset($_SESSION['uid'])) { // get the currency from the logged in client
       $uid = $_SESSION['uid'];
       $user = Capsule::table('tblclients')
           ->where('id', $uid)
       $curr = $user->currency;
   $return['uid'] = $uid;
   $return['curr'] = $curr;

   if (isset($curr)) { // get the price of the new product
       $pricing = Capsule::table('tblpricing')
           ->where('relid', $pid)
           ->where('type', 'product')
           ->where('currency', $curr)
       switch($newBillingCycle) {
           case 'monthly':
               $newFullPrice = $pricing->monthly;
           case 'quarterly':
               $newFullPrice = $pricing->quarterly;
           case 'semiannually':
               $newFullPrice = $pricing->semiannually;
           case 'annually':
               $newFullPrice = $pricing->annually;
           case 'biennially':
               $newFullPrice = $pricing->biennially;
           case 'triennially':
               $newFullPrice = $pricing->triennially;
   $return['newFullPrice'] = $newFullPrice;

   if ($price != 0 && isset($newFullPrice)) {
       $debit = round($newFullPrice * $daysLeft / $newTotalDays, 2); // calculate what the debit amount was 
       $credit = $debit - $price; // calculate what the credit was (the old price may have changed) 
       $discRate = round($di****ount / $price, 2); //assumes a discount rate with no decimal part
       $newDebit = round((1 - $discRate) * $newFullPrice * $daysLeft / $newTotalDays, 2);
       $newPrice = round(($newDebit - $credit) / (1 - $discRate), 2);
       $newDi****ount = round($newPrice * $discRate, 2);

       $return['debit'] = $debit;
       $return['credit'] = $credit;
       $return['discRate'] = $discRate;
       $return['newDebit'] = $newDebit;

       $return['price'] = $newPrice;
       $return['discount'] = $newDi****ount;

   logModuleCall('myAddon', 'OrderProductUpgradeOverride', $vars, $vars, $return, array());

   return $return;

add_hook('OrderProductUpgradeOverride', 1, 'myAddon_OrderProductUpgradeOverride');


Initial checks seem good so far. Changing to a new billing cycle looks good too.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...

Important Information

By using this site, you agree to our Terms of Use & Guidelines and understand your posts will initially be pre-moderated