Event discounts / coupons for event registration

Published
2009-05-21 23:47
Written by

A Make It Happen to move this into core for CiviCRM is currently running for v4.1. You can help make it happen by contributing here

Discounting events has been requested on the forums a few times. It also popped up both on IRC yesterday and was also needed on civicrm.org for our upcoming UK Training Camp. We decided to see if we could implement this via hooks and minimal / no changes to the core code. Turned out we can implement this this feature using three civicrm hooks and a helper fuction. We improved the core a wee bit by allowing the buildForm hook to inform the tpl system of any form elements added. The tpl then places these form elements at the top of the form. An event can have zero or more coupons. Coupons for an event are stored in the option group / value table. Each coupon code is a string (chosen by admin) and can be used a certain number of times. Coupons can also be used forever. A valid coupon gives the user a percentage discount on the event. Giving the coupon a 100% value basically gives the user a free registration. Coupons can be sent in via the URL or can be entered into the registration form. Here is the recipe for making it work for an event sign-up page -

  • In a drupal module (lets call it civitest), Implement civicrm_buildAmount hook to add a form field to accept the coupon code. As the code checks for coupon value in both get and post request, this step is optional if coupon is being passed by url - e.g http://...civicrm/event/register?id=X&reset=1&discountCode=123456789.
    
    function civitest_civicrm_buildForm( $formName, &$form ) {
        if ( $formName == 'CRM_Event_Form_Registration_Register' &&
             $form->getVar( '_eventId' ) == 3 ) { //use event id here
            $form->addElement( 'text', 'discountCode', ts( 'Discount Code' ) );
            // also assign to template
            $template =& CRM_Core_Smarty::singleton( );
            $beginHookFormElements = $template->get_template_vars( 'beginHookFormElements' );
            if ( ! $beginHookFormElements ) { $beginHookFormElements = array( ); }
            $beginHookFormElements[] = 'discountCode';
            $form->assign( 'beginHookFormElements', $beginHookFormElements );
            $discountCode = CRM_Utils_Request::retrieve( 'discountCode', 'String', $form, false, null, $_REQUEST );
            if ( $discountCode ) {
                $defaults = array( 'discountCode' => $discountCode );
                $form->setDefaults( $defaults );
            }
        }
    }
    
    
  • Implement civicrm_buildAmount hook to apply the discount.
    
    function civitest_civicrm_buildAmount( $pageType, &$form, &$amount ) {
        $eventID = $form->getVar( '_eventId' );
        if ( $pageType != 'event' ||
             $eventID != 3 ) { // use event ID here
            return;
        }
        $discountCode = CRM_Utils_Request::retrieve( 'discountCode', 'String', $form, false, null, $_REQUEST );
        if ( ! $discountCode ) { return; }
        list( $discountID, $discountPercent, $discountNumber ) = _civitest_discountHelper( $eventID, $discountCode );
        if ( $discountNumber <= 0 ) { return; }
        foreach ( $amount as $amountId => $amountInfo ) {
            $amount[$amountId]['value'] = $amount[$amountId]['value'] -
                ceil($amount[$amountId]['value'] * $discountPercent / 100);
            $amount[$amountId]['label'] = $amount[$amountId]['label'] .
                "\t - with {$discountPercent}% discount";
        }
    }
    
  • Implement civicrm_postProcess hook to decrement the coupon usage counter by one. Ah! yes coupon code can be configured to be used for one / finite / infinite number of times :)
        
    function civitest_civicrm_postProcess( $class, &$form ) {
        $eventID = $form->getVar( '_eventId' );
        if ( ! is_a($form, 'CRM_Event_Form_Registration_Confirm') || $eventID != 3 ) { return; }
            
        $discountCode = CRM_Utils_Request::retrieve( 'discountCode', 'String', $form, false, null, $_REQUEST );
        if ( ! $discountCode ) { return; }
        list( $discountID, $discountPercent, $discountNumber ) = _civitest_discountHelper( $eventID, $discountCode );
        if ( ! $discountID || $discountNumber <= 0 || $discountNumber == 123456789 ) { return; }
        $query = "UPDATE civicrm_option_value v SET v.weight = v.weight - 1 WHERE  v.id = %1 AND v.weight > 0";
        $params = array( 1 => array( $discountID, 'Integer' ) );
        CRM_Core_DAO::executeQuery( $query, $params );
    }
    
  • Use following query to add a coupon (say '1234XYZ5678') to your database with a validity of two usage.
    
    INSERT INTO `civicrm_option_group` (`name`, `description`, `is_reserved`, `is_active`) 
    VALUES ('event_discount_3', 'Event Discount', 0, 1); # 3 is the event ID here
    SELECT @option_group_id_ed := max(id) from civicrm_option_group where name = 'event_discount_3';
    INSERT INTO `civicrm_option_value` (`option_group_id`, `label`, `value`, `name`, `weight`, `is_active`) 
    VALUES (@option_group_id_ed, 'Discount Code', 50, '1234XYZ5678', 2, 1); 
    
    weight=2 implies this coupon can't be used after second usage. value=50 indicates the discount percent. 50% for this coupon. name='1234XYZ5678' is the coupon code.
  • Helper function that was used while implmenting the hooks above -
    
    function _civitest_discountHelper( $eventID, $discountCode ) {
        $sql = "
    SELECT v.id as id, v.value as value, v.weight as weight
    FROM   civicrm_option_value v,
           civicrm_option_group g
    WHERE  v.option_group_id = g.id
    AND    v.name = %1
    AND    g.name = %2";
        $params = array( 1 => array( $discountCode              , 'String' ),
                         2 => array( "event_discount_{$eventID}", 'String' ) );
        $dao = CRM_Core_DAO::executeQuery( $sql, $params );
        if ( $dao->fetch( ) ) {
            // ensure discountPercent is a valid numeric number <= 100
            if ( $dao->value && is_numeric( $dao->value ) && $dao->value >= 0 &&
                 $dao->value <= 100 && is_numeric( $dao->weight ) ) {
                return array( $dao->id, $dao->value, $dao->weight );
            }
        }
        return array( null, null, null );
    }
    

As the whole functionality looks powerful, team is on a discussion to add this feature to core in a future release

Filed under

Comments

Anonymous (not verified)
2009-05-22 - 10:47

Just wondering how / if that could work with a price set? Perhaps each price element needs an attribute to indicate that it is "discountable"?

price sets requires a redesign and some thinking about how to structure it better for this and other requests. We expect this to happen in a 3.x release. If important to you / your org and you want it ina earlier release consider sponsoring the work / retaining a developer

How to check discount for additional particpants. Any idea. If we set the discount code for main participant the additional participants get the discounted amount instead of original amount.

nice feature :)

especially like template access through hooks - very cool. Reckon I can use this template to move lots of my customiz/sations out of core and into hooks. supercool.

Anonymous (not verified)
2009-06-22 - 15:03

Thanks for sharing this solution, we look forward to future releases that include this in UI, but will happily use this for now. Keep up the great work.

Anonymous (not verified)
2009-08-01 - 14:27

This is cool, But was wondering if it can be implemented in conjunction with other payment methods.. meaning i'd like to offer vouchers for free/prepaid entry.

So that the event manager can issue vouchers to groups, And regular people outside of them groups can register at full price.

Thanks again for this, It's awesome!

Anonymous (not verified)
2009-10-12 - 14:48

We have been using this little gem for a while now, upgraded a staging site to 3.0 (great work by the way!), however we are unable to get this working in 3.0.1 (field won't even display on the form now). Are major changes required to make this compatible?

David Greenberg on forum post http://forum.civicrm.org/index.php/topic,10194.0.html says

"Took a quick look at the hooks used in that example - and they're certainly all available for 3.0 and 3.1. Not sure why the one poster reported a problem - but might have been a coding issue ..."

Anonymous (not verified)
2010-01-24 - 08:47

I'm looking at this and this is exactly what I need for a site I'm building but, not being too versed in coding, I can't figure out from these instructions how to implement. Silly question: Since it says "In a drupal module (lets call it civitest), Implement civicrm_buildAmount hook to add a form field to accept the coupon code," couldn't some kind heart provide a Drupal module for this and add it to Drupal's module repository? It looks to me as if there are other steps required other than just setting up a module, so, if provided, perhaps the module I am requesting could provide, in its README file, a step-by-step (geared for us proverbial "dummies") so that even I could implement this type of discount?

http://civicrm.org/professional/

and create a flexible discounting module that implements a lot of the UI and allows users to pick the event and discounts. That will help your ORG and the community

lobo

Anonymous (not verified)
2010-03-01 - 05:46

Thanks very much for the code. I have it working nicely in a joomla instance with civicrm 3.1.3.

Forgive my ignorance here, I'm new with civicrm, but I wonder why the author chose to add a form field via code. Can't we add a custom field in the admin panel and reference it in code?

I was about to try doing that so the discount field is the last form field in my registration instead of the first.

might be better to discuss on the forums. The form field is ignored if the discount code is sent in via the GET parameter

lobo

BTW, as of 3.3.x the buildAmount hook supports manipulating price sets. See the civitest.module.sample for example code.