In our usage most of the discount codes will be for 100% discounts - the user will have already paid for the full membership. So there needs to be a usable way to hide the billing requirements if they are going to enter a code. The quickest way to this end (but maybe not the best) is to use the "Pay later" checkbox in CiviCRM and rewrite the option as "Already Paid or will Pay Later" or something like that. It hides the billing nicely.

The drawback is that it doesn't enforce the requirement of the discount code. So it would still allow someone to defer payment and not enter the discount.

To some extent this would require some javascript on the contribution form so I'm not sure how much could be done in this module. But at least myself or others could post solutions here.

Comments

dharmatech’s picture

Nubeli,

Yes it requires some jquery magic to hide the billing fields. We blogged about it and submitted an issue and patch a couple of months ago to address membership and billing info w/discounts. Not much we can do about it from the module side unless the showHideBilling() jquery function gets added to core templates.

dharmatech’s picture

An additional problem is the validation of the code and processing of the price happens in the next screen via the hook_civicrm_buildAmount().

nubeli’s picture

Right, that is a problem too. I'm looking into how the "pay later" option prevents that. I am playing with adding a checkbox to show the discount code box and hide the billing info. So I could add a check for that checkbox when building the amount.

nubeli’s picture

So here's the approach we are taking to allow the by-pass of the billing information with a 100% discount (which means zero dollars charged). I added a checkbox to civievent_discount_civicrm_buildForm():

if (in_array($fname, $display_forms)) {
        $form->addElement('checkbox', 'skipbilling', NULL, ts('Skip billing and use discount code'));
        $form->addElement('text', 'discountcode', ts('Discount code'));
      }

      $template =& CRM_Core_Smarty::singleton();
      $bhfe = $template->get_template_vars('beginHookFormElements');
      if (!$bhfe) { $bhfe = array(); }
      $bhfe[] = 'skipbilling';
      $bhfe[] = 'discountcode';
      $form->assign('beginHookFormElements', $bhfe);
      $code = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
      $skipbilling = CRM_Utils_Request::retrieve('skipbilling', 'String', $form, false, '', $_REQUEST);
      if ($code || $skipbilling) {
        $defaults = array('discountcode' => $code, 'skipbilling' => $skipbilling);
        $form->setDefaults($defaults);
        
        if (!in_array($fname, $display_forms)) {
          $form->addElement('hidden', 'discountcode', $code);
          $form->addElement('hidden', 'skipbilling', $skipbilling);
        }

I could have figured out how to add the element in a custom module but it would have been a bit of a bother. So for now it's in civievent_discount.

I also created a custom module to do some validation based on the values of the "skipbilling" checkbox I added. It doesn't need to be separate, but figured I'd put it separate as I work on it. When the skipbilling checkbox is checked then it won't require the billing information fields, but will require the discount code field. It will only work if a discount code is set for 100% otherwise it requires the billing information and sends an error.

function codes_custom_civicrm_validate( $formName, &$fields, &$files, &$form ) {

  $errors = array();
  
  $skipbilling = $form->_submitValues['skipbilling'];
  
  $code_ret = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
  module_load_include('module', 'civievent_discount', 'civievent_discount');
  $code = _get_code_details($code_ret);
  
  if (is_a($form, 'CRM_Contribute_Form_Contribution_Main')) {
  
    if ($skipbilling == TRUE) {
      if ($code['amount'] == 100 && $code['amount_type'] == 'P') {
        $form->setElementError('credit_card_type', NULL);
        $form->setElementError('credit_card_number', NULL);
        $form->setElementError('cvv2', NULL);
        $form->setElementError('credit_card_exp_date', NULL);
        $form->setElementError('billing_first_name', NULL);
        $form->setElementError('billing_last_name', NULL);
        $form->setElementError('billing_street_address-5', NULL);
        $form->setElementError('billing_city-5', NULL);
        $form->setElementError('billing_country_id-5', NULL);
        $form->setElementError('billing_state_province_id-5', NULL);
        $form->setElementError('billing_postal_code-5', NULL);
      }
      else if ($code['amount'] < 100 && $code['amount_type'] == 'P') {
        $errors['discountcode'] = ts('Skipping billing only works for 100% discounts.');
      }
      
      if (empty($code) && $code_ret == '') {
        $errors['discountcode'] = ts('Discount code is required if skipping billing.');
      }
    }
    
    else if (!isset($skipbilling)) {
      $form->setElementError('discountcode', NULL);
    }
  }
  return empty( $errors ) ? true : $errors;
}

The problem that arose was that it was now possible to get a membership for free if someone just entered a wrong discount code! So I ended up moving the validation from civievent_discount_civicrm_membershipTypeValues() to civievent_discount_civicrm_validate(). It worked at least in CiviCRM 3.3.5:

else if ($name == 'CRM_Contribute_Form_Contribution_Main') {
    $code = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
    if (empty($code)) { return; } // could be the first time page loads
    if ($form->getVar('_name') == 'ThankYou') { return; } // ignore thank you page
    $code = _get_code_details($code); 

    if (empty($code)) {
      $errors['discountcode'] = ts('Discount code is not valid for this membership.');
    }
    if (!is_null($code['expiration']) && $code['expiration'] != '0000-00-00 00:00:00') {
      $time = format_date(time(), 'custom', 'Y-m-d H:i:s', variable_get('date_default_timezone', 0));
      if (strtotime($time) > strtotime($code['expiration'])) {
        $errors['discountcode'] = ts('The discount code has expired.');
      }
    }
    if ($code['count_max'] > 0 && $code['count_use'] >= $code['count_max']) {
      $errors['discountcode'] = ts('There are not enough uses remaining for this code.');
    }
  }

I had also added some jquery to a custom template for the contribution form (this is optional) to hide the billing fields when the skipbilling checkbox is checked. (I originally hid the discount field too but then it won't work for less 100% discounts.) So at CRM/Contribute/Form/Contribution/Main.tpl (make a copy of the default template and put in your own custom template) and added the not-so-elegant javascript:

{* If user checks "Skip Billing" box then hide billing. *}
<script language="JavaScript" type="text/javascript">
{literal}

$("input[name='skipbilling']").click(function () {
  var skip = $("#skipbilling").is(':checked');
  if (skip == false) {
    $('#payment_information').show();
    $('.amount_other-section').show();
    if ($('#amount_other').val() == '0.00') {
      $('#amount_other').val('');
    }
  }
  else if (skip == true) {
    $('#payment_information').hide();
    $('.amount_other-section').hide();
    $('#amount_other').val('0.00');
  }
});

$(document).ready(function(){
  var skip = $("#skipbilling").is(':checked');

  if (skip == true) {
    $('#payment_information').hide();
    $('.amount_other-section').hide();
    $('#amount_other').val('0.00');
  }
});
{/literal}
</script>

I'm not going to add my copy of civievent_discount.module file since I think there are some other things in it that aren't in the current dev version. Hopefully that's enough info to figure this out. If you want to roll it into the dev version feel free and I won't have to maintain a custom module. This would be useful for anyone who is giving out 100% discounts and the user knows they've paid for the full amount already so don't want to bother with filling in the billing information for no reason.

nubeli’s picture

I decided to merge my changes into the May2, 2011 dev version. The civievent_discount_civicrm_membershipTypeValues() is still minimized as the validation is moved to hook_validate. Now it also checks that the code submitted matches the chosen membership level, in addition to checking for invalid/expired codes. Sorry for not providing proper patches.

function civievent_discount_civicrm_validate($name, &$fields, &$files, &$form) {
  if (in_array($name, array(
      'CRM_Event_Form_Participant',
      'CRM_Event_Form_Registration_Register'))) {

    $code = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
    if ($code == '') { return; }
    $code = _get_code_details($code);
    if (!$code) {
      require_once('civievent_discount.admin.inc');
      $codes = _get_discounts();
      $code = _verify_autodiscount($codes);
    }
    if (empty($code)) {
      $errors['discountcode'] = ts('Discount code is invalid.');
      return $errors;
    } else {
      $sv = $form->getVar('_submitValues');
      $apcount = $sv['additional_participants'];

      if ($code['count_max'] > 0) {
        if (empty($apcount)) { $apcount = 1; }
        else { $apcount++; } // add 1 for person registering
        if (($code['count_use'] + $apcount) > $code['count_max']) {
          $errors['discountcode'] = ts('There are not enough uses remaining for this code.');
        } else {
          if (!is_null($code['expiration']) && $code['expiration'] != '0000-00-00 00:00:00') {
            $time = format_date(time(), 'custom', 'Y-m-d H:i:s', variable_get('date_default_timezone', 0));
            if (strtotime($time) > strtotime($code['expiration'])) {
            $errors['discountcode'] = ts('The discount code has expired.');
            }
          }
        }
      }
    }
  }
  else if (in_array($name, array('CRM_Contribute_Form_Contribution_Main'))) {
    $code = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
    $select_membership = CRM_Utils_Request::retrieve('selectMembership', 'String', $form, false, null, $_REQUEST);
    if (empty($code)) { return; } // could be the first time page loads
    if ($form->getVar('_name') == 'ThankYou') { return; } // ignore thank you page
    $code = _get_code_details($code); 

    if (empty($code)) {
      $errors['discountcode'] = ts('Discount code is not valid.');
    }
    else if (!in_array($select_membership, unserialize($code['memberships']))) {
      $errors['discountcode'] = ts('Invalid discount code for this membership level.');
    }
    
    if (!is_null($code['expiration']) && $code['expiration'] != '0000-00-00 00:00:00') {
      $time = format_date(time(), 'custom', 'Y-m-d H:i:s', variable_get('date_default_timezone', 0));
      if (strtotime($time) > strtotime($code['expiration'])) {
        $errors['discountcode'] = ts('The discount code has expired.');
      }
    }
    if ($code['count_max'] > 0 && $code['count_use'] >= $code['count_max']) {
      $errors['discountcode'] = ts('There are not enough uses remaining for this code.');
    }
  }
  return empty($errors) ? true : $errors;
}

And I improved my custom validation so that it only checks if the discount is 100%:


function codes_custom_civicrm_validate( $formName, &$fields, &$files, &$form ) {

  $errors = array();
  
  $skipbilling = $form->_submitValues['skipbilling'];
  
  $code_ret = CRM_Utils_Request::retrieve('discountcode', 'String', $form, false, null, $_REQUEST);
  module_load_include('module', 'civievent_discount', 'civievent_discount');
  $code = _get_code_details($code_ret);

  if (is_a($form, 'CRM_Contribute_Form_Contribution_Main')) {
  
    if ($skipbilling == TRUE) {
      
      $form->setElementError('credit_card_type', NULL);
        $form->setElementError('credit_card_number', NULL);
        $form->setElementError('cvv2', NULL);
        $form->setElementError('credit_card_exp_date', NULL);
        $form->setElementError('billing_first_name', NULL);
        $form->setElementError('billing_last_name', NULL);
        $form->setElementError('billing_street_address-5', NULL);
        $form->setElementError('billing_city-5', NULL);
        $form->setElementError('billing_country_id-5', NULL);
        $form->setElementError('billing_state_province_id-5', NULL);
        $form->setElementError('billing_postal_code-5', NULL);
      
      if ($code['amount'] == 100 && $code['amount_type'] == 'P') {
        
      }
      else if ($code['amount'] < 100 && $code['amount_type'] == 'P') {
        $errors['discountcode'] = ts('Skipping billing only works for 100% discounts.');
      }
      
      if (empty($code) && $code_ret == '') {
        $errors['discountcode'] = ts('Discount code is required if skipping billing.');
      }
    }
    
    else if (!isset($skipbilling)) {
      $form->setElementError('discountcode', NULL);
    }
  }
  return empty( $errors ) ? true : $errors;
}
nubeli’s picture

A much easier way to include the js seems obvious now that I've seen it done. Include the js in civievent_discount_civicrm_buildForm() like this:

drupal_add_js(drupal_get_path('module','civievent_discount') . '/skipbilling.js');

dharmatech’s picture

Assigned: Unassigned » dharmatech
Status: Active » Closed (won't fix)

Cleaning up old tickets. Marking as "closed (won't fix)" since it requires editing the templates. I'll add it to the project page under Theming.