Merge remote-tracking branch 'upstream/4.6' into 4.6-master-2015-10-14-11-04-09
[civicrm-core.git] / CRM / Core / Payment / PayPalImpl.php
index db76d97e72094ed7a20de331bd500f8a107a7f91..0aa2061ea6c2630839c32d33b2a9b13e5695cc0d 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
  +--------------------------------------------------------------------+
- | CiviCRM version 4.6                                                |
+ | CiviCRM version 4.7                                                |
  +--------------------------------------------------------------------+
  | Copyright CiviCRM LLC (c) 2004-2015                                |
  +--------------------------------------------------------------------+
  +--------------------------------------------------------------------+
  */
 
+use Civi\Payment\Exception\PaymentProcessorException;
+
 /**
  *
  * @package CRM
  * @copyright CiviCRM LLC (c) 2004-2015
- * $Id$
- *
+ */
+
+/**
+ * Class CRM_Core_Payment_PayPalImpl for paypal pro, paypal standard & paypal express.
  */
 class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
   const CHARSET = 'iso-8859-1';
 
   protected $_mode = NULL;
 
-  /**
-   * We only need one instance of this object. So we use the singleton
-   * pattern and cache the instance in this variable
-   *
-   * @var object
-   */
-  static private $_singleton = NULL;
-
   /**
    * Constructor.
    *
@@ -69,9 +65,6 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
       $this->_processorName = ts('PayPal Express');
     }
 
-    if (!$this->_paymentProcessor['user_name']) {
-      CRM_Core_Error::fatal(ts('Could not find user name for payment processor'));
-    }
   }
 
   /**
@@ -85,6 +78,69 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return FALSE;
   }
 
+  /**
+   * Does this processor support pre-approval.
+   *
+   * This would generally look like a redirect to enter credentials which can then be used in a later payment call.
+   *
+   * Currently Paypal express supports this, with a redirect to paypal after the 'Main' form is submitted in the
+   * contribution page. This token can then be processed at the confirm phase. Although this flow 'looks' like the
+   * 'notify' flow a key difference is that in the notify flow they don't have to return but in this flow they do.
+   *
+   * @return bool
+   */
+  protected function supportsPreApproval() {
+    if ($this->_processorName == ts('PayPal Express') || $this->_processorName == ts('PayPal Pro')) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Opportunity for the payment processor to override the entire form build.
+   *
+   * @param CRM_Core_Form $form
+   *
+   * @return bool
+   *   Should form building stop at this point?
+   */
+  public function buildForm(&$form) {
+    if ($this->_processorName == 'PayPal Express' || $this->_processorName == 'PayPal Pro') {
+      $this->addPaypalExpressCode($form);
+      if ($this->_processorName == 'PayPal Express') {
+        CRM_Core_Region::instance('billing-block-post')->add(array(
+          'template' => 'CRM/Financial/Form/PaypalExpress.tpl',
+          'name' => 'paypal_express',
+        ));
+      }
+      if ($this->_processorName == 'PayPal Pro') {
+        CRM_Core_Region::instance('billing-block-pre')->add(array(
+          'template' => 'CRM/Financial/Form/PaypalPro.tpl',
+        ));
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Billing mode button is basically synonymous with paypal express  - this is probably a good example of 'odds & sods' code we
+   * need to find a way for the payment processor to assign. A tricky aspect is that the payment processor may need to set the order
+   *
+   * @param CRM_Core_Form $form
+   */
+  protected function addPaypalExpressCode(&$form) {
+    if (empty($form->isBackOffice)) {
+      $form->_expressButtonName = $form->getButtonName('upload', 'express');
+      $form->assign('expressButtonName', $form->_expressButtonName);
+      $form->add(
+        'image',
+        $form->_expressButtonName,
+        $this->_paymentProcessor['url_button'],
+        array('class' => 'crm-form-submit')
+      );
+    }
+  }
+
   /**
    * Can recurring contributions be set against pledges.
    *
@@ -101,6 +157,21 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return TRUE;
   }
 
+  /**
+   * Default payment instrument validation.
+   *
+   * Implement the usual Luhn algorithm via a static function in the CRM_Core_Payment_Form if it's a credit card
+   * Not a static function, because I need to check for payment_type.
+   *
+   * @param array $values
+   * @param array $errors
+   */
+  public function validatePaymentInstrument($values, &$errors) {
+    if ($this->_paymentProcessor['payment_processor_type'] == 'PayPal_Pro') {
+      CRM_Core_Payment_Form::validateCreditCard($values, $errors);
+    }
+  }
+
   /**
    * Express checkout code. Check PayPal documentation for more information
    *
@@ -110,18 +181,18 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
    * @return array
    *   the result in an nice formatted array (or an error object)
    */
-  public function setExpressCheckOut(&$params) {
+  protected function setExpressCheckOut(&$params) {
     $args = array();
 
     $this->initialize($args, 'SetExpressCheckout');
 
-    $args['paymentAction'] = $params['payment_action'];
+    $args['paymentAction'] = 'Sale';
     $args['amt'] = $params['amount'];
     $args['currencyCode'] = $params['currencyID'];
     $args['desc'] = CRM_Utils_Array::value('description', $params);
     $args['invnum'] = $params['invoiceID'];
-    $args['returnURL'] = $params['returnURL'];
-    $args['cancelURL'] = $params['cancelURL'];
+    $args['returnURL'] = $this->getReturnSuccessUrl($params['qfKey']);
+    $args['cancelURL'] = $this->getCancelUrl($params['qfKey'], NULL);
     $args['version'] = '56.0';
 
     //LCD if recurring, collect additional data and set some values
@@ -146,6 +217,20 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return $result['token'];
   }
 
+  /**
+   * Get any details that may be available to the payment processor due to an approval process having happened.
+   *
+   * In some cases the browser is redirected to enter details on a processor site. Some details may be available as a
+   * result.
+   *
+   * @param array $storedDetails
+   *
+   * @return array
+   */
+  public function getPreApprovalDetails($storedDetails) {
+    return empty($storedDetails['token']) ? array() : $this->getExpressCheckoutDetails($storedDetails['token']);
+  }
+
   /**
    * Get details from paypal. Check PayPal documentation for more information
    *
@@ -170,22 +255,21 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     }
 
     /* Success */
-
-    $params = array();
-    $params['token'] = $result['token'];
-    $params['payer_id'] = $result['payerid'];
-    $params['payer_status'] = $result['payerstatus'];
-    $params['first_name'] = $result['firstname'];
-    $params['middle_name'] = CRM_Utils_Array::value('middlename', $result);
-    $params['last_name'] = $result['lastname'];
-    $params['street_address'] = $result['shiptostreet'];
-    $params['supplemental_address_1'] = CRM_Utils_Array::value('shiptostreet2', $result);
-    $params['city'] = $result['shiptocity'];
-    $params['state_province'] = $result['shiptostate'];
-    $params['postal_code'] = $result['shiptozip'];
-    $params['country'] = $result['shiptocountrycode'];
-
-    return $params;
+    $fieldMap = array(
+      'token' => 'token',
+      'payer_status' => 'payerstatus',
+      'payer_id' => 'payerid',
+      'first_name' => 'firstname',
+      'middle_name' => 'middlename',
+      'last_name' => 'lastname',
+      'street_address' => 'shiptostreet',
+      'supplemental_address_1' => 'shiptostreet2',
+      'city' => 'shiptocity',
+      'postal_code' => 'shiptozip',
+      'state_province' => 'shiptostate',
+      'country' => 'shiptocountrycode',
+    );
+    return $this->mapPaypalParamsToCivicrmParams($fieldMap, $result);
   }
 
   /**
@@ -199,18 +283,21 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
    *   the result in an nice formatted array (or an error object)
    */
   public function doExpressCheckout(&$params) {
+    $statuses = CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id');
+    if (!empty($params['is_recur'])) {
+      return $this->createRecurringPayments($params);
+    }
     $args = array();
 
     $this->initialize($args, 'DoExpressCheckoutPayment');
-
     $args['token'] = $params['token'];
-    $args['paymentAction'] = $params['payment_action'];
+    $args['paymentAction'] = 'Sale';
     $args['amt'] = $params['amount'];
     $args['currencyCode'] = $params['currencyID'];
     $args['payerID'] = $params['payer_id'];
     $args['invnum'] = $params['invoiceID'];
-    $args['returnURL'] = CRM_Utils_Array::value('returnURL', $params);
-    $args['cancelURL'] = CRM_Utils_Array::value('cancelURL', $params);
+    $args['returnURL'] = $this->getReturnSuccessUrl($params['qfKey']);
+    $args['cancelURL'] = $this->getCancelUrl($params['qfKey'], NULL);
     $args['desc'] = $params['description'];
 
     // add CiviCRM BN code
@@ -219,7 +306,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     $result = $this->invokeAPI($args);
 
     if (is_a($result, 'CRM_Core_Error')) {
-      return $result;
+      throw new PaymentProcessorException(CRM_Core_Error::getMessages($result));
     }
 
     /* Success */
@@ -233,7 +320,13 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     }
     $params['payment_status'] = $result['paymentstatus'];
     $params['pending_reason'] = $result['pendingreason'];
-
+    if (!empty($params['is_recur'])) {
+      // See comment block.
+      $result['payment_status_id'] = array_search('Pending', $statuses);
+    }
+    else {
+      $result['payment_status_id'] = array_search('Completed', $statuses);
+    }
     return $params;
   }
 
@@ -245,14 +338,14 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
    */
   public function createRecurringPayments(&$params) {
     $args = array();
-
+    // @todo this function is riddled with enotices - perhaps use $this->mapPaypalParamsToCivicrmParams($fieldMap, $result)
     $this->initialize($args, 'CreateRecurringPaymentsProfile');
 
     $start_time = strtotime(date('m/d/Y'));
     $start_date = date('Y-m-d\T00:00:00\Z', $start_time);
 
     $args['token'] = $params['token'];
-    $args['paymentAction'] = $params['payment_action'];
+    $args['paymentAction'] = 'Sale';
     $args['amt'] = $params['amount'];
     $args['currencyCode'] = $params['currencyID'];
     $args['payerID'] = $params['payer_id'];
@@ -268,7 +361,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     $args['totalbillingcycles'] = $params['installments'];
     $args['version'] = '56.0';
     $args['profilereference'] = "i={$params['invoiceID']}" .
-      "&m=$component" .
+      "&m=" .
       "&c={$params['contactID']}" .
       "&r={$params['contributionRecurID']}" .
       "&b={$params['contributionID']}" .
@@ -310,6 +403,41 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     $args['method'] = $method;
   }
 
+  /**
+   * Process payment - this function wraps around both doTransferPayment and doDirectPayment.
+   *
+   * The function ensures an exception is thrown & moves some of this logic out of the form layer and makes the forms
+   * more agnostic.
+   *
+   * Payment processors should set payment_status_id. This function adds some historical defaults ie. the
+   * assumption that if a 'doDirectPayment' processors comes back it completed the transaction & in fact
+   * doTransferCheckout would not traditionally come back.
+   *
+   * doDirectPayment does not do an immediate payment for Authorize.net or Paypal so the default is assumed
+   * to be Pending.
+   *
+   * Once this function is fully rolled out then it will be preferred for processors to throw exceptions than to
+   * return Error objects
+   *
+   * @param array $params
+   *
+   * @param string $component
+   *
+   * @return array
+   *   Result array
+   *
+   * @throws \Civi\Payment\Exception\PaymentProcessorException
+   */
+  public function doPayment(&$params, $component = 'contribute') {
+    if ($this->_paymentProcessor['payment_processor_type'] != 'PayPal_Express'
+    && (!empty($params['credit_card_number']) && empty($params['token']))
+    ) {
+      return parent::doPayment($params, $component);
+    }
+    $this->_component = $component;
+    return $this->doExpressCheckout($params);
+  }
+
   /**
    * This function collects all the information from a web/api form and invokes
    * the relevant payment processor specific functions to perform the transaction
@@ -326,7 +454,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
 
     $this->initialize($args, 'DoDirectPayment');
 
-    $args['paymentAction'] = $params['payment_action'];
+    $args['paymentAction'] = 'Sale';
     $args['amt'] = $params['amount'];
     $args['currencyCode'] = $params['currencyID'];
     $args['invnum'] = $params['invoiceID'];
@@ -438,14 +566,6 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
   public function checkConfig() {
     $error = array();
     $paymentProcessorType = CRM_Core_PseudoConstant::paymentProcessorType(FALSE, NULL, 'name');
-    if (
-      $this->_paymentProcessor['payment_processor_type_id'] == CRM_Utils_Array::key('PayPal_Standard', $paymentProcessorType) ||
-      $this->_paymentProcessor['payment_processor_type_id'] == CRM_Utils_Array::key('PayPal', $paymentProcessorType)
-    ) {
-      if (empty($this->_paymentProcessor['user_name'])) {
-        $error[] = ts('User Name is not set in the Administer &raquo; System Settings &raquo; Payment Processors.');
-      }
-    }
 
     if ($this->_paymentProcessor['payment_processor_type_id'] != CRM_Utils_Array::key('PayPal_Standard', $paymentProcessorType)) {
       if (empty($this->_paymentProcessor['signature'])) {
@@ -456,6 +576,9 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
         $error[] = ts('Password is not set in the Administer &raquo; System Settings &raquo; Payment Processors.');
       }
     }
+    if (empty($this->_paymentProcessor['user_name'])) {
+      $error[] = ts('User Name is not set in the Administer &raquo; System Settings &raquo; Payment Processors.');
+    }
 
     if (!empty($error)) {
       return implode('<p>', $error);
@@ -494,6 +617,19 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return parent::isSupported($method);
   }
 
+  /**
+   * Paypal express replaces the submit button with it's own.
+   *
+   * @return bool
+   *   Should the form button by suppressed?
+   */
+  public function isSuppressSubmitButtons() {
+    if ($this->_paymentProcessor['payment_processor_type'] == 'PayPal_Express') {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
   /**
    * @param string $message
    * @param array $params
@@ -599,6 +735,28 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return FALSE;
   }
 
+  /**
+   * Function to action pre-approval if supported
+   *
+   * @param array $params
+   *   Parameters from the form
+   *
+   * @return array
+   *   - pre_approval_parameters (this will be stored on the calling form & available later)
+   *   - redirect_url (if set the browser will be redirected to this.
+   */
+  public function doPreApproval(&$params) {
+    if (!isset($params['button']) || !stristr($params['button'], 'express')) {
+      return array();
+    }
+    $this->_component = $params['component'];
+    $token = $this->setExpressCheckOut($params);
+    return array(
+      'pre_approval_parameters' => array('token' => $token),
+      'redirect_url' => $this->_paymentProcessor['url_site'] . "/cgi-bin/webscr?cmd=_express-checkout&token=$token",
+    );
+  }
+
   /**
    * @param array $params
    * @param string $component
@@ -846,7 +1004,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
 
   /**
    * This function will take NVPString and convert it to an Associative Array and it will decode the response.
-   * It is usefull to search for a particular key and displaying arrays.
+   * It is useful to search for a particular key and displaying arrays.
    * @nvpstr is NVPString.
    * @nvpArray is Associative Array.
    */
@@ -854,7 +1012,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     $result = array();
 
     while (strlen($str)) {
-      // postion of key
+      // position of key
       $keyPos = strpos($str, '=');
 
       // position of value
@@ -873,4 +1031,35 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
     return $result;
   }
 
+  /**
+   * Get array of fields that should be displayed on the payment form.
+   *
+   * @return array
+   * @throws CiviCRM_API3_Exception
+   */
+  public function getPaymentFormFields() {
+    if ($this->_processorName == ts('PayPal Pro')) {
+      return $this->getCreditCardFormFields();
+    }
+    else {
+      return array();
+    }
+  }
+
+  /**
+   * Map the paypal params to CiviCRM params using a field map.
+   *
+   * @param array $fieldMap
+   * @param array $paypalParams
+   *
+   * @return array
+   */
+  protected function mapPaypalParamsToCivicrmParams($fieldMap, $paypalParams) {
+    $params = array();
+    foreach ($fieldMap as $civicrmField => $paypalField) {
+      $params[$civicrmField] = isset($paypalParams[$paypalField]) ? $paypalParams[$paypalField] : NULL;
+    }
+    return $params;
+  }
+
 }