INFRA-132 - CRM/Contribute - Misc
[civicrm-core.git] / CRM / Core / Payment.php
index a7f21fde65ced9332a25850779b9afd1318d88e2..7345ab67bf7f84d3a7a48b3ce412b6021e202bd6 100644 (file)
@@ -1,9 +1,9 @@
 <?php
 /*
  +--------------------------------------------------------------------+
- | CiviCRM version 4.4                                                |
+ | CiviCRM version 4.6                                                |
  +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2013                                |
+ | Copyright CiviCRM LLC (c) 2004-2014                                |
  +--------------------------------------------------------------------+
  | This file is a part of CiviCRM.                                    |
  |                                                                    |
@@ -28,7 +28,7 @@
 /**
  *
  * @package CRM
- * @copyright CiviCRM LLC (c) 2004-2013
+ * @copyright CiviCRM LLC (c) 2004-2014
  * $Id$
  *
  */
 abstract class CRM_Core_Payment {
 
   /**
-   * how are we getting billing information?
+   * How are we getting billing information?
    *
    * FORM   - we collect it on the same page
    * BUTTON - the processor collects it and sends it back to us via some protocol
    */
-  CONST
+  const
     BILLING_MODE_FORM = 1,
     BILLING_MODE_BUTTON = 2,
     BILLING_MODE_NOTIFY = 4;
 
   /**
-   * which payment type(s) are we using?
+   * Which payment type(s) are we using?
    *
    * credit card
    * direct debit
    * or both
    *
    */
-  CONST
+  const
     PAYMENT_TYPE_CREDIT_CARD = 1,
     PAYMENT_TYPE_DIRECT_DEBIT = 2;
 
@@ -63,7 +63,7 @@ abstract class CRM_Core_Payment {
    * START, END
    *
    */
-  CONST
+  const
     RECURRING_PAYMENT_START = 'START',
     RECURRING_PAYMENT_END = 'END';
 
@@ -78,38 +78,50 @@ abstract class CRM_Core_Payment {
 
   protected $_paymentProcessor;
 
+  /**
+   * @var CRM_Core_Form
+   */
   protected $_paymentForm = NULL;
 
   /**
-   * singleton function used to manage this object
+   * Singleton function used to manage this object
    *
-   * @param string  $mode the mode of operation: live or test
-   * @param object  $paymentProcessor the details of the payment processor being invoked
-   * @param object  $paymentForm      reference to the form object if available
-   * @param boolean $force            should we force a reload of this payment object
+   * @param string $mode
+   *   The mode of operation: live or test.
+   * @param array $paymentProcessor
+   *   The details of the payment processor being invoked.
+   * @param object $paymentForm
+   *   Deprecated - avoid referring to this if possible. If you have to use it document why as this is scary interaction.
+   * @param bool $force
+   *   Should we force a reload of this payment object.
    *
-   * @return object
+   * @return CRM_Core_Payment
+   * @return \CRM_Core_Payment
+   * @throws \CRM_Core_Exception
    * @static
-   *
    */
-  static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) {
+  public static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) {
     // make sure paymentProcessor is not empty
     // CRM-7424
     if (empty($paymentProcessor)) {
       return CRM_Core_DAO::$_nullObject;
     }
 
-    $cacheKey = "{$mode}_{$paymentProcessor['id']}_" . (int)isset($paymentForm);
+    $cacheKey = "{$mode}_{$paymentProcessor['id']}_" . (int) isset($paymentForm);
+
     if (!isset(self::$_singleton[$cacheKey]) || $force) {
       $config = CRM_Core_Config::singleton();
       $ext = CRM_Extension_System::singleton()->getMapper();
       if ($ext->isExtensionKey($paymentProcessor['class_name'])) {
         $paymentClass = $ext->keyToClass($paymentProcessor['class_name'], 'payment');
-        require_once ($ext->classToPath($paymentClass));
+        require_once $ext->classToPath($paymentClass);
       }
       else {
         $paymentClass = 'CRM_Core_' . $paymentProcessor['class_name'];
-        require_once (str_replace('_', DIRECTORY_SEPARATOR, $paymentClass) . '.php');
+        if (empty($paymentClass)) {
+          throw new CRM_Core_Exception('no class provided');
+        }
+        require_once str_replace('_', DIRECTORY_SEPARATOR, $paymentClass) . '.php';
       }
 
       //load the object.
@@ -117,45 +129,312 @@ abstract class CRM_Core_Payment {
     }
 
     //load the payment form for required processor.
-    if ($paymentForm !== NULL) {
-      self::$_singleton[$cacheKey]->setForm($paymentForm);
-    }
+    //if ($paymentForm !== NULL) {
+    //self::$_singleton[$cacheKey]->setForm($paymentForm);
+    //}
 
     return self::$_singleton[$cacheKey];
   }
 
+  /**
+   * @param array $params
+   *
+   * @return mixed
+   */
+  public static function logPaymentNotification($params) {
+    $message = 'payment_notification ';
+    if (!empty($params['processor_name'])) {
+      $message .= 'processor_name=' . $params['processor_name'];
+    }
+    if (!empty($params['processor_id'])) {
+      $message .= 'processor_id=' . $params['processor_id'];
+    }
+
+    $log = new CRM_Utils_SystemLogger();
+    $log->alert($message, $_REQUEST);
+  }
+
+  /**
+   * Check if capability is supported
+   * @param string $capability
+   *   E.g BackOffice, LiveMode, FutureRecurStartDate.
+   *
+   * @return bool
+   */
+  public function supports($capability) {
+    $function = 'supports' . ucfirst($capability);
+    if (method_exists($this, $function)) {
+      return $this->$function();
+    }
+    return FALSE;
+  }
+
+  /**
+   * Are back office payments supported - e.g paypal standard won't permit you to enter a credit card associated with someone else's login
+   * The intention is to support off-site (other than paypal) & direct debit but that is not all working yet so to reach a 'stable' point we disable
+   * @return bool
+   */
+  protected function supportsBackOffice() {
+    if ($this->_paymentProcessor['billing_mode'] == 4 || $this->_paymentProcessor['payment_type'] != 1) {
+      return FALSE;
+    }
+    else {
+      return TRUE;
+    }
+  }
+
+  /**
+   * Are live payments supported - e.g dummy doesn't support this
+   * @return bool
+   */
+  protected function supportsLiveMode() {
+    return TRUE;
+  }
+
+  /**
+   * Are test payments supported
+   * @return bool
+   */
+  protected function supportsTestMode() {
+    return TRUE;
+  }
+
+  /**
+   * Should the first payment date be configurable when setting up back office recurring payments
+   * We set this to false for historical consistency but in fact most new processors use tokens for recurring and can support this
+   * @return bool
+   */
+  protected function supportsFutureRecurStartDate() {
+    return FALSE;
+  }
+
   /**
    * Setter for the payment form that wants to use the processor
    *
-   * @param obj $paymentForm
+   * @param CRM_Core_Form $paymentForm
    *
    */
-  function setForm(&$paymentForm) {
+  public function setForm(&$paymentForm) {
     $this->_paymentForm = $paymentForm;
   }
 
   /**
    * Getter for payment form that is using the processor
    *
-   * @return obj  A form object
+   * @return CRM_Core_Form  A form object
    */
-  function getForm() {
+  public function getForm() {
     return $this->_paymentForm;
   }
 
   /**
    * Getter for accessing member vars
    *
+   * @param string $name
+   *
+   * @return null
    */
-  function getVar($name) {
+  public function getVar($name) {
     return isset($this->$name) ? $this->$name : NULL;
   }
 
+  /**
+   * Get name for the payment information type
+   *
+   * @return string
+   */
+  public function getPaymentTypeName() {
+    return $this->_paymentProcessor['payment_type'] == 1 ? 'credit_card' : 'direct_debit';
+  }
+
+  /**
+   * Get label for the payment information type
+   *
+   * @return string
+   */
+  public function getPaymentTypeLabel() {
+    return $this->_paymentProcessor['payment_type'] == 1 ? 'Credit Card' : 'Direct Debit';
+  }
+
+  /**
+   * Get array of fields that should be displayed on the payment form
+   * @todo make payment type an option value & use it in the function name - currently on debit & credit card work
+   * @return array
+   * @throws CiviCRM_API3_Exception
+   */
+  public function getPaymentFormFields() {
+    if ($this->_paymentProcessor['billing_mode'] == 4) {
+      return array();
+    }
+    return $this->_paymentProcessor['payment_type'] == 1 ? $this->getCreditCardFormFields() : $this->getDirectDebitFormFields();
+  }
+
+  /**
+   * Get array of fields that should be displayed on the payment form for credit cards
+   *
+   * @return array
+   */
+  protected function getCreditCardFormFields() {
+    return array(
+      'credit_card_type',
+      'credit_card_number',
+      'cvv2',
+      'credit_card_exp_date',
+    );
+  }
+
+  /**
+   * Get array of fields that should be displayed on the payment form for direct debits
+   *
+   * @return array
+   */
+  protected function getDirectDebitFormFields() {
+    return array(
+      'account_holder',
+      'bank_account_number',
+      'bank_identification_number',
+      'bank_name',
+    );
+  }
+
+  /**
+   * Return an array of all the details about the fields potentially required for payment fields
+   * Only those determined by getPaymentFormFields will actually be assigned to the form
+   *
+   * @return array field metadata
+   */
+  public function getPaymentFormFieldsMetadata() {
+    //@todo convert credit card type into an option value
+    $creditCardType = array('' => ts('- select -')) + CRM_Contribute_PseudoConstant::creditCard();
+    return array(
+      'credit_card_number' => array(
+        'htmlType' => 'text',
+        'name' => 'credit_card_number',
+        'title' => ts('Card Number'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 20,
+          'maxlength' => 20,
+          'autocomplete' => 'off'
+        ),
+        'is_required' => TRUE,
+      ),
+      'cvv2' => array(
+        'htmlType' => 'text',
+        'name' => 'cvv2',
+        'title' => ts('Security Code'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 5,
+          'maxlength' => 10,
+          'autocomplete' => 'off'
+        ),
+        'is_required' => CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::CONTRIBUTE_PREFERENCES_NAME,
+          'cvv_backoffice_required',
+          NULL,
+          1
+        ),
+        'rules' => array(
+          array(
+            'rule_message' => ts('Please enter a valid value for your card security code. This is usually the last 3-4 digits on the card\'s signature panel.'),
+            'rule_name' => 'integer',
+            'rule_parameters' => NULL,
+        )),
+      ),
+      'credit_card_exp_date' => array(
+        'htmlType' => 'date',
+        'name' => 'credit_card_exp_date',
+        'title' => ts('Expiration Date'),
+        'cc_field' => TRUE,
+        'attributes' => CRM_Core_SelectValues::date('creditCard'),
+        'is_required' => TRUE,
+        'rules' => array(
+          array(
+            'rule_message' => ts('Card expiration date cannot be a past date.'),
+            'rule_name' => 'currentDate',
+            'rule_parameters' => TRUE,
+          )),
+      ),
+      'credit_card_type' => array(
+        'htmlType' => 'select',
+        'name' => 'credit_card_type',
+        'title' => ts('Card Type'),
+        'cc_field' => TRUE,
+        'attributes' => $creditCardType,
+        'is_required' => FALSE,
+      ),
+      'account_holder' => array(
+        'htmlType' => 'text',
+        'name' => 'account_holder',
+        'title' => ts('Account Holder'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 20,
+          'maxlength' => 34,
+          'autocomplete' => 'on'
+        ),
+        'is_required' => TRUE,
+      ),
+      //e.g. IBAN can have maxlength of 34 digits
+      'bank_account_number' => array(
+        'htmlType' => 'text',
+        'name' => 'bank_account_number',
+        'title' => ts('Bank Account Number'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 20,
+          'maxlength' => 34,
+          'autocomplete' => 'off'
+        ),
+        'rules' => array(
+          array(
+            'rule_message' => ts('Please enter a valid Bank Identification Number (value must not contain punctuation characters).'),
+            'rule_name' => 'nopunctuation',
+            'rule_parameters' => NULL,
+        )),
+        'is_required' => TRUE,
+      ),
+      //e.g. SWIFT-BIC can have maxlength of 11 digits
+      'bank_identification_number' => array(
+        'htmlType' => 'text',
+        'name' => 'bank_identification_number',
+        'title' => ts('Bank Identification Number'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 20,
+          'maxlength' => 11,
+          'autocomplete' => 'off'
+        ),
+        'is_required' => TRUE,
+        'rules' => array(
+          array(
+            'rule_message' => ts('Please enter a valid Bank Identification Number (value must not contain punctuation characters).'),
+            'rule_name' => 'nopunctuation',
+            'rule_parameters' => NULL,
+        )),
+      ),
+      'bank_name' => array(
+        'htmlType' => 'text',
+        'name' => 'bank_name',
+        'title' => ts('Bank Name'),
+        'cc_field' => TRUE,
+        'attributes' => array(
+          'size' => 20,
+          'maxlength' => 64,
+          'autocomplete' => 'off'
+        ),
+        'is_required' => TRUE,
+
+      )
+    );
+  }
+
   /**
    * This function collects all the information from a web/api form and invokes
    * the relevant payment processor specific functions to perform the transaction
    *
-   * @param  array $params assoc array of input parameters for this transaction
+   * @param array $params
+   *   Assoc array of input parameters for this transaction.
    *
    * @return array the result in an nice formatted array (or an error object)
    * @abstract
@@ -163,16 +442,42 @@ abstract class CRM_Core_Payment {
   abstract function doDirectPayment(&$params);
 
   /**
-   * This function checks to see if we have the right config values
+   * Process payment - this function wraps around both doTransferPayment and doDirectPayment
+   * it ensures an exception is thrown & moves some of this logic out of the form layer and makes the forms more agnostic
    *
-   * @param  string $mode the mode we are operating in (live or test)
+   * @param array $params
+   *
+   * @param $component
+   *
+   * @throws CRM_Core_Exception
+   */
+  public function doPayment(&$params, $component) {
+    if ($this->_paymentProcessor['billing_mode'] == 4) {
+      $result = $this->doTransferCheckout($params, $component);
+    }
+    else {
+      $result = $this->doDirectPayment($params, $component);
+    }
+    if (is_a($result, 'CRM_Core_Error')) {
+      throw new CRM_Core_Exception(CRM_Core_Error::getMessages($result));
+    }
+    //CRM-15767 - Submit Credit Card Contribution not being saved
+    return $result;
+  }
+
+  /**
+   * This function checks to see if we have the right config values
    *
    * @return string the error message if any
-   * @public
    */
   abstract function checkConfig();
 
-  static function paypalRedirect(&$paymentProcessor) {
+  /**
+   * @param $paymentProcessor
+   *
+   * @return bool
+   */
+  public static function paypalRedirect(&$paymentProcessor) {
     if (!$paymentProcessor) {
       return FALSE;
     }
@@ -190,9 +495,8 @@ abstract class CRM_Core_Payment {
 
   /**
    * Page callback for civicrm/payment/ipn
-   * @public
    */
-  static function handleIPN() {
+  public static function handleIPN() {
     self::handlePaymentMethod(
       'PaymentNotification',
       array(
@@ -209,12 +513,14 @@ abstract class CRM_Core_Payment {
    * processor & ideally the processor will be validating the results
    * Load requested payment processor and call that processor's handle<$method> method
    *
-   * @public
+   * @param $method
+   * @param array $params
    */
-  static function handlePaymentMethod($method, $params = array( )) {
+  public static function handlePaymentMethod($method, $params = array()) {
     if (!isset($params['processor_id']) && !isset($params['processor_name'])) {
       CRM_Core_Error::fatal("Either 'processor_id' or 'processor_name' param is required for payment callback");
     }
+    self::logPaymentNotification($params);
 
     // Query db for processor ..
     $mode = @$params['mode'];
@@ -260,7 +566,10 @@ abstract class CRM_Core_Payment {
       }
       else {
         // Legacy or extension as module instance
-        $paymentClass = 'CRM_Core_' . $dao->class_name;
+        if (empty($paymentClass)) {
+          $paymentClass = 'CRM_Core_' . $dao->class_name;
+
+        }
       }
 
       $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($dao->processor_id, $mode);
@@ -287,10 +596,11 @@ abstract class CRM_Core_Payment {
       $extension_instance_found = TRUE;
     }
 
-    if (!$extension_instance_found) CRM_Core_Error::fatal(
+    if (!$extension_instance_found) CRM_Core_Error::fatal(
       "No extension instances of the '{$params['processor_name']}' payment processor were found.<br />" .
       "$method method is unsupported in legacy payment processors."
-    );
+      );
+    }
 
     // Exit here on web requests, allowing just the plain text response to be echoed
     if ($method == 'handlePaymentNotification') {
@@ -299,90 +609,91 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Function to check whether a method is present ( & supported ) by the payment processor object.
+   * Check whether a method is present ( & supported ) by the payment processor object.
    *
-   * @param  string $method method to check for.
+   * @param string $method
+   *   Method to check for.
    *
    * @return boolean
-   * @public
    */
-  function isSupported($method = 'cancelSubscription') {
+  public function isSupported($method = 'cancelSubscription') {
     return method_exists(CRM_Utils_System::getClassName($this), $method);
   }
 
-  function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
-    if ($action == 'cancel') {
-      $url = 'civicrm/contribute/unsubscribe';
-    }
-    elseif ($action == 'billing') {
-      if (!$this->isSupported('updateSubscriptionBillingInfo')) {
-        return NULL;
-      }
-      $url = 'civicrm/contribute/updatebilling';
-    }
-    elseif ($action == 'update') {
-      $url = 'civicrm/contribute/updaterecur';
+  /**
+   * @param int $entityID
+   * @param null $entity
+   * @param string $action
+   *
+   * @return string
+   */
+  public function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
+    // Set URL
+    switch ($action) {
+      case 'cancel':
+        $url = 'civicrm/contribute/unsubscribe';
+        break;
+
+      case 'billing':
+        //in notify mode don't return the update billing url
+        if (!$this->isSupported('updateSubscriptionBillingInfo')) {
+          return NULL;
+        }
+        $url = 'civicrm/contribute/updatebilling';
+        break;
+
+      case 'update':
+        $url = 'civicrm/contribute/updaterecur';
+        break;
     }
+
     $session       = CRM_Core_Session::singleton();
     $userId        = $session->get('userID');
-    $checksumValue = "";
-
-    if ($entityID && $entity == 'membership') {
-      if (!$userId) {
-        $contactID     = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id");
-        $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf');
-        $checksumValue = "&cs={$checksumValue}";
-      }
-      return CRM_Utils_System::url($url, "reset=1&mid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE);
-    }
-
-    if ($entityID && $entity == 'contribution') {
-      if (!$userId) {
-        $contactID     = CRM_Core_DAO::getFieldValue("CRM_Contribute_DAO_Contribution", $entityID, "contact_id");
-        $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf');
-        $checksumValue = "&cs={$checksumValue}";
-      }
-      return CRM_Utils_System::url($url, "reset=1&coid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE);
-    }
-
-    if ($entityID && $entity == 'recur') {
-      if (!$userId) {
-        $sql = "
+    $contactID     = 0;
+    $checksumValue = '';
+    $entityArg     = '';
+
+    // Find related Contact
+    if ($entityID) {
+      switch ($entity) {
+        case 'membership':
+          $contactID = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id");
+          $entityArg = 'mid';
+          break;
+
+        case 'contribution':
+          $contactID = CRM_Core_DAO::getFieldValue("CRM_Contribute_DAO_Contribution", $entityID, "contact_id");
+          $entityArg = 'coid';
+          break;
+
+        case 'recur':
+          $sql = "
     SELECT con.contact_id
       FROM civicrm_contribution_recur rec
 INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
      WHERE rec.id = %1
   GROUP BY rec.id";
-        $contactID     = CRM_Core_DAO::singleValueQuery($sql, array(1 => array($entityID, 'Integer')));
-        $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf');
-        $checksumValue = "&cs={$checksumValue}";
+          $contactID = CRM_Core_DAO::singleValueQuery($sql, array(1 => array($entityID, 'Integer')));
+          $entityArg = 'crid';
+          break;
+      }
+    }
+
+    // Add entity arguments
+    if ($entityArg != '') {
+      // Add checksum argument
+      if ($contactID != 0 && $userId != $contactID) {
+        $checksumValue = '&cs=' . CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf');
       }
-      return CRM_Utils_System::url($url, "reset=1&crid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE);
+      return CRM_Utils_System::url($url, "reset=1&{$entityArg}={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE);
     }
 
+    // Else login URL
     if ($this->isSupported('accountLoginURL')) {
       return $this->accountLoginURL();
     }
-    return $this->_paymentProcessor['url_recur'];
-  }
 
-  /**
-   * Check for presence of type 1 or type 3 enabled processors (means we can do back-office submit credit/debit card trxns)
-   * @public
-   */
-  static function allowBackofficeCreditCard($template = NULL, $variableName = 'newCredit') {
-    $newCredit = FALSE;
-    // restrict to type=1 (credit card) payment processor payment_types and only include billing mode types 1 and 3
-    $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE,
-      "billing_mode IN ( 1, 3 ) AND payment_type = 1"
-    );
-    if (count($processors) > 0) {
-      $newCredit = TRUE;
-    }
-    if ($template) {
-      $template->assign($variableName, $newCredit);
-    }
-    return $newCredit;
+    // Else default
+    return $this->_paymentProcessor['url_recur'];
   }
-
 }