You are here

pay_method_gateway.inc in Pay 7

Same filename and directory in other branches
  1. 6 includes/handlers/pay_method_gateway.inc

The base class for credit card payment activities.

File

includes/handlers/pay_method_gateway.inc
View source
<?php

/**
 * @file
 * The base class for credit card payment activities.
 */
class pay_method_gateway extends pay_method {
  var $payment_types = array();
  var $gateway_testmode = TRUE;

  // Each subclass should define these according to capabilities.
  var $gateway_supports_ach = FALSE;
  var $gateway_supports_cc = TRUE;
  var $gateway_supports_recurring = FALSE;
  var $payment_type;
  var $cc_type;
  var $cc_number;
  var $cc_ccv2;
  var $cc_exp_month;
  var $cc_exp_year;
  var $cc_issue_number;

  /**
   * Effect an 'authorize' gateway transaction.  If successful, the transaction
   * state will be set to 'active' and the authorization history will be stored
   * in our records.
   *
   * @return string the recommended transaction state after this action.
   *  Possible states are listed in $pay_transaction->states().
   */
  function authorize_action() {

    // Call execute, which runs the gateway interactions.
    $result = $this
      ->execute();

    // We can't proceed if there's an error, so return 'canceled'.
    return $result ? 'active' : 'canceled';
  }

  /**
   * Complete a transaction.  For new transactions, this means creating an
   * AUTH_CAPTURE request.  For existing ones, capture a preauthorization.
   *
   * @return string the recommended transaction state after this action.
   *  Possible states are listed in $pay_transaction->states().
   */
  function complete_action() {

    // Look for an 'authorize' action in this activity's history.
    foreach ($this->activity
      ->history() as $previous) {

      // If there was a successful authorization, copy its details.
      if ($previous->action == 'authorize' && $previous->result) {
        $this->activity->identifier = $previous->identifier;
        $this->activity->data = $previous->data;
        $this->activity->total = $previous->total;
        $this->payment_type = $previous->payment_type;
      }
    }

    // TODO this should be deprecated: 'activity' has been renamed to 'action'.
    $this->activity->activity = $this->activity->action;

    // TODO passing in $this->activity should also be deprecated.
    $result = $this
      ->execute($this->activity);

    // If successful, increase the 'captured' balance.
    if ($result) {
      $this->activity
        ->set_transaction_total($this->activity->total);
    }

    // If the transaction's state is 'pending', this is an auth_capture
    // action. We don't have enough to complete it, so return a failed state.
    if ($this->activity
      ->pay_transaction()
      ->state() == 'pending') {
      return $result ? 'complete' : 'canceled';
    }
    elseif ($this->activity
      ->pay_transaction()
      ->state() == 'active') {
      return $result ? 'complete' : 'active';
    }
  }

  /**
   * Refund a transaction.
   */
  function refund_action() {
  }

  /**
   * Cancel a transaction.
   */
  function cancel_action() {

    // TODO - If there's anything to void, void it!
    return parent::cancel_action();
  }

  /**
   * The URL where your provider accepts requests.
   *
   * Returning NULL prevents anything from executing. Subclasses are responsible
   * for returning a responsible url.
   */
  function gateway_url() {
  }

  /**
   * The request data that will be sent to your gatway provider.
   *
   * Returning NULL prevents anything from executing. Subclasses are responsible
   * for returning responsible request data.
   */
  function gateway_request() {
  }

  /**
   * HTTP headers that should be sent to the gateway provider.
   *
   * For example, if your gateway provider requires XML:
   *
   *  function gateway_headers() {
   *     return array('Content-Type' => 'text/xml');
   *  }
   */
  function gateway_headers() {
    return array();
  }
  function gateway_response() {
  }
  function execute($activity) {
    if ($request = $this
      ->gateway_request()) {
      $ret = drupal_http_request($this
        ->gateway_url(), array(
        'headers' => $this
          ->gateway_headers(),
        'method' => 'POST',
        'data' => $request,
      ));
      if (isset($ret->error)) {
        watchdog('payment', "Gateway Error: @err Payment NOT processed.", array(
          '@err' => $ret->error,
        ));
        drupal_set_message(t('We were unable to process your credit card payment. Please verify your card details and try again. If the problem persists, contact us to complete your order.'), 'error');
        $this->activity->data = (array) $ret;
        $this->activity->result = FALSE;
      }
      else {
        $this->activity->result = $this
          ->gateway_response($ret->data);
      }

      // Return TRUE or FALSE on success/failure.
      return $this->activity->result == 1;
    }
  }
  function cc_expiration() {
    if ($this->cc_exp_month && $this->cc_exp_year) {
      $exp = str_pad((int) $this->cc_exp_month, 2, '0', STR_PAD_LEFT);
      $exp .= str_pad((int) $this->cc_exp_year, 2, '0', STR_PAD_LEFT);
      return $exp;
    }
  }
  function cc_issue_number_required($val = NULL) {
    $issue_number_cc_types = array(
      'switch',
    );

    // For validation of form submission.
    if ($val && in_array($val, $issue_number_cc_types)) {
      return TRUE;
    }

    // For display of form field.
    if (!$val) {
      foreach ($issue_number_cc_types as $cc_type) {
        if ($this->payment_types[$cc_type]) {
          return TRUE;
        }
      }
    }
    return FALSE;
  }
  function set_cc_issue_number($val) {
    $this->cc_issue_number = preg_replace('/[^\\d]/', '', $val);
  }
  function set_cc_number($val) {
    $this->cc_number = preg_replace('/[^\\d]/', '', $val);

    // Set the credit card type rather than relying on user input.
    $orig_cc_type = $this->cc_type;
    $this->cc_type = NULL;
    $prefix1 = substr($this->cc_number, 0, 1);
    $prefix2 = substr($this->cc_number, 0, 2);
    $prefix3 = substr($this->cc_number, 0, 3);
    $prefix4 = substr($this->cc_number, 0, 4);
    $prefix5 = substr($this->cc_number, 0, 5);
    $prefix6 = substr($this->cc_number, 0, 6);

    /**
     * References: http://www.beachnet.com/~hstiles/cardtype.html
     */
    switch ($prefix1) {
      case 1:

        // JCB: prefix 1800, length 15
        if ($prefix4 == 1800) {
          $this->cc_type = 'jcb';
        }
        break;
      case 2:

        // JCB: prefix 2131, length 15
        if ($prefix4 == 2131) {
          $this->cc_type = 'jcb';
        }
        elseif ($prefix4 == 2014) {
          $this->cc_type = 'enroute';
        }
        elseif ($prefix4 == 2149) {
          $this->cc_type = 'enroute';
        }
        break;
      case 3:

        // Diners Club: prefix 300-305, length 14
        if (in_array($prefix3, array(
          300,
          301,
          302,
          303,
          304,
          305,
        ))) {
          $this->cc_type = 'diners';
        }
        elseif (in_array($prefix2, array(
          34,
          37,
        ))) {
          $this->cc_type = 'amex';
        }
        elseif (in_array($prefix2, array(
          36,
          38,
        ))) {
          $this->cc_type = 'diners';
        }
        else {
          $this->cc_type = 'jcb';
        }
        break;
      case 4:

        // Visa: prefix 4, length 13, 16
        $this->cc_type = 'visa';

        // Switch: prefix 4903, 4905, 4911, 4936
        if (in_array($prefix4, array(
          4903,
          4905,
          4911,
          4936,
        ))) {
          $this->cc_type = 'switch';
        }
        break;
      case 5:

        // Mastercard: prefix 51-55, length 16
        if (in_array($prefix2, array(
          51,
          52,
          53,
          54,
          55,
        ))) {
          $this->cc_type = 'mc';
        }

        // Switch: prefix 564182
        if ($prefix6 == 564182) {
          $this->cc_type = 'switch';
        }

        // Maestro: prefix 5018, 5020, 5038
        if (in_array($prefix4, array(
          5018,
          5020,
          5038,
        ))) {
          $this->cc_type = 'maestro';
        }
        break;
      case 6:

        // Discover: prefix 6011, length 16
        if ($prefix4 == 6011) {
          $this->cc_type = 'discover';
        }

        // Laser: prefix 6304 or 6706 or 6709 or 6771, length 19
        if ($prefix4 == 6304 || $prefix4 == 6706 || $prefix4 == 6709 || $prefix4 == 6771) {
          $this->cc_type = 'laser';
        }

        // Switch: prefix 633110, 6333, 6759,
        if ($prefix6 == 633110 || $prefix4 == 6333) {
          $this->cc_type = 'switch';
        }

        // Maestro: prefix 6759, 6761, 6763
        if (in_array($prefix4, array(
          6759,
          6761,
          6763,
        ))) {
          $this->cc_type = 'maestro';
        }

        // Solo: prefix 6334 or 6767
        if ($prefix4 == 6334 || $prefix4 == 6767) {
          $this->cc_type = 'solo';
        }
        break;
    }

    // Only if we still fail to determine card type, reset it to user supplied
    // value.
    if (empty($this->cc_type)) {
      $this->cc_type = $orig_cc_type;
    }
  }
  function set_cc_ccv2($val) {
    $this->cc_ccv2 = preg_replace('/[^\\d]/', '', $val);
  }
  function cc_number_validate() {
    $total = 0;
    $number = $this->cc_number;
    if (strlen($number) < 13 || strlen($number) > 19) {
      $this->error_message = t('Invalid credit card number.');
      return FALSE;
    }

    // Mod-10 credit card validation.
    for ($i = 0; $i < strlen($number); $i++) {
      $digit = substr($number, $i, 1);
      if ((strlen($number) - $i - 1) % 2) {
        $digit *= 2;
        if ($digit > 9) {
          $digit -= 9;
        }
      }
      $total += $digit;
    }
    if ($total % 10 != 0) {
      $this->error_message = t('Invalid credit card number.');
      return FALSE;
    }
    return TRUE;
  }
  function cc_expiration_validate() {
    if ($this->cc_exp_year == date('y')) {
      if ($this->cc_exp_month < date('m')) {
        $this->error_message = t('The credit card appears to be expired.');
        return FALSE;
      }
    }
    return TRUE;
  }
  function cc_issue_number_validate() {
    if ($this
      ->cc_issue_number_required($this->cc_type) && (strlen($this->cc_issue_number) != 2 || !is_numeric($this->cc_issue_number))) {
      $this->error_message = t('Invalid issue number.');
      return FALSE;
    }
    return TRUE;
  }
  function cc_ccv2_validate() {
    if ($this->cc_ccv2) {
      if ($this->cc_type == 'amex') {
        if (strlen($this->cc_ccv2) != 4) {
          $this->error_message = t('The security code must be 4 numeric digits for American Express cards');
          return FALSE;
        }
      }
      elseif (strlen($this->cc_ccv2) != 3) {
        $this->error_message = t('The security code must be 3 numeric digits.');
        return FALSE;
      }
    }
    return TRUE;
  }
  function payment_types($filter = NULL) {
    $payment_types = array();
    if ($this->gateway_supports_cc) {
      $payment_types = array(
        'visa' => t('Visa'),
        'mc' => t('Mastercard'),
        'amex' => t('American Express'),
        'discover' => t('Discover'),
        'diners' => t("Diner's Club"),
        'laser' => t('Laser'),
        'maestro' => t('Maestro'),
        'switch' => t('Switch'),
        'solo' => t('Solo'),
      );
    }
    if ($this->gateway_supports_ach) {

      // TODO this is a stub. We need to build the echeck form and validation
      // functions for this to be relevant. Thus it's commented out for now.

      //$payment_types['ach'] = t('Online checking transfer.');
    }
    if ($filter) {
      foreach ($payment_types as $key => $label) {
        if (!$filter[$key]) {
          unset($payment_types[$key]);
        }
      }
    }
    return $payment_types;
  }
  function settings_form(&$form, &$form_state) {
    parent::settings_form($form, $form_state);
    $group = $this
      ->handler();
    $form[$group]['gateway'] = array(
      '#type' => 'fieldset',
      '#title' => t('Gateway settings'),
      '#group' => $group,
    );
    $form[$group]['gateway']['payment_types'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Available payment types'),
      '#options' => $this
        ->payment_types(),
      '#default_value' => $this->payment_types,
      '#required' => TRUE,
      '#parents' => array(
        $group,
        'payment_types',
      ),
    );
    $form[$group]['gateway']['gateway_testmode'] = array(
      '#type' => 'checkbox',
      '#title' => t('Test mode'),
      '#default_value' => $this->gateway_testmode,
      '#parents' => array(
        $group,
        'gateway_testmode',
      ),
    );
  }
  function form(&$form, &$form_state) {
    global $user;
    $group = $this->pay_form
      ->handler();
    if (isset($this->gateway_testmode) && $this->gateway_testmode) {
      drupal_set_message(t('The @name payment method is in test mode. This transaction will not be fully processed.', array(
        '@name' => $this
          ->title(),
      )), 'warning');
    }
    if (isset($form[$group]['pay_method'][$this->pmid])) {
      $method_form = $form[$group]['pay_method'][$this->pmid];
    }
    $method_form['#theme'] = 'pay_cc_form';
    $options = array();
    if ($this->gateway_supports_cc) {
      $options['cc'] = t('Credit or debit card');
    }

    // TODO should reflect 'bank account' input here, but it's unsupported.
    $method_form['payment_type'] = array(
      '#type' => 'select',
      '#title' => t('Payment type'),
      '#options' => $options,
      '#required' => TRUE,
      '#default_value' => 'cc',
      '#access' => count($options) > 1,
    );
    $method_form['first_name'] = array(
      '#type' => 'textfield',
      '#title' => t('First name'),
      '#pre_render' => array(
        'pay_element_set_required',
      ),
    );
    $method_form['last_name'] = array(
      '#type' => 'textfield',
      '#title' => t('Last name'),
      '#pre_render' => array(
        'pay_element_set_required',
      ),
    );
    if ($user->uid) {
      $method_form['mail'] = array(
        '#type' => 'value',
        '#value' => $user->mail,
      );
    }
    else {
      $method_form['mail'] = array(
        '#type' => 'textfield',
        '#title' => t('E-mail'),
        '#pre_render' => array(
          'pay_element_set_required',
        ),
      );
    }
    $method_form['billto'] = array(
      '#type' => 'postal',
      '#postal_user' => $user,
      '#title' => t('Billing address'),
      '#pre_render' => array(
        'pay_element_set_required',
      ),
    );
    $method_form['cc_type'] = array(
      '#type' => 'radios',
      '#title' => t('Card type'),
      '#options' => $this
        ->payment_types($this->payment_types),
    );
    $method_form['cc_number'] = array(
      '#type' => 'textfield',
      '#title' => t('Card number'),
      '#size' => 19,
      '#maxlength' => 19,
      '#pre_render' => array(
        'pay_element_set_required',
      ),
      '#attributes' => array(
        'autocomplete' => 'off',
      ),
    );
    $method_form['cc_ccv2'] = array(
      '#type' => 'textfield',
      '#title' => t('Security code'),
      '#size' => 4,
      '#maxlength' => 4,
      '#attributes' => array(
        'autocomplete' => 'off',
      ),
    );
    if ($this
      ->cc_issue_number_required()) {
      $method_form['cc_issue_number'] = array(
        '#type' => 'textfield',
        '#title' => t('Issue number'),
        '#size' => 2,
        '#maxlength' => 2,
        '#attributes' => array(
          'autocomplete' => 'off',
        ),
      );
    }
    $months = array();
    foreach (range(1, 12) as $i) {
      $key = str_pad($i, 2, '0', STR_PAD_LEFT);
      $months[$key] = $key . '- ' . date('F', strtotime('1-' . $i . '-2000'));
    }
    $method_form['cc_exp_month'] = array(
      '#type' => 'select',
      '#options' => $months,
      '#pre_render' => array(
        'pay_element_set_required',
      ),
      '#attributes' => array(
        'title' => t('Expiration month'),
      ),
    );
    $years = array();
    foreach (range(0, 9) as $i) {
      $year = date('Y') + $i;
      $years[substr($year, 2)] = $year;
    }
    $method_form['cc_exp_year'] = array(
      '#type' => 'select',
      '#options' => $years,
      '#pre_render' => array(
        'pay_element_set_required',
      ),
      '#attributes' => array(
        'title' => t('Expiration year'),
      ),
    );

    // Add this method_form to the expected place on the parent form.
    $form[$group]['pay_method'][$this->pmid] = $method_form;
    $form[$group]['pay_method'][$this->pmid]['#prefix'] = '<div class="payment-method-form">';
    $form[$group]['pay_method'][$this->pmid]['#suffix'] = '</div>';
  }

  // This is called from the form_validate function in a pay_form class.

  /**
   * @todo Please document this function.
   * @see http://drupal.org/node/1354
   */
  function pay_method_validate($form, &$form_state, $element) {
    parent::pay_method_validate($form, $form_state, $element);
    if ($this->payment_type == 'cc') {

      // Manually handle #required on fields required by this payment_type.
      $required = array(
        'first_name',
        'last_name',
        'mail',
        'cc_number',
        'cc_exp_month',
        'cc_exp_year',
      );
      foreach ($required as $key) {
        if ((!count($this->{$key}) || is_string($this->{$key}) && strlen(trim($this->{$key})) == 0) && isset($element[$key])) {
          form_error($element[$key], t('!name field is required.', array(
            '!name' => $element[$key]['#title'],
          )));
        }
      }

      // Validate the card number and set the form value to our clean version.
      if (!$this
        ->cc_number_validate()) {
        form_error($element['cc_number'], $this->error_message);
      }
      form_set_value($element['cc_number'], $this->cc_number, $form_state);

      // Validate the CCV2 code and set the form value to our clean version.
      if (!$this
        ->cc_ccv2_validate()) {
        form_error($element['cc_ccv2'], $this->error_message);
      }
      form_set_value($element['cc_ccv2'], $this->cc_ccv2, $form_state);

      // Validate the expiration date.
      if (!$this
        ->cc_expiration_validate()) {
        form_error($element['cc_exp_month'], $this->error_message);
      }

      // Set the "payment_type" value to the specific card type.
      form_set_value($element['payment_type'], $this->cc_type, $form_state);

      // Validate the issue number field.
      if (!$this
        ->cc_issue_number_validate()) {
        form_set_error('cc_issue_number', $this->error_message);
      }
    }
  }

}

Classes

Namesort descending Description
pay_method_gateway @file The base class for credit card payment activities.