<?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.
*
$this->_processorName = ts('PayPal Express');
}
- if (!$this->_paymentProcessor['user_name']) {
- CRM_Core_Error::fatal(ts('Could not find user name for payment processor'));
- }
}
/**
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.
*
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
*
* @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
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
*
}
/* 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);
}
/**
* 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
$result = $this->invokeAPI($args);
if (is_a($result, 'CRM_Core_Error')) {
- return $result;
+ throw new PaymentProcessorException(CRM_Core_Error::getMessages($result));
}
/* Success */
}
$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;
}
*/
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'];
$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']}" .
$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
$this->initialize($args, 'DoDirectPayment');
- $args['paymentAction'] = $params['payment_action'];
+ $args['paymentAction'] = 'Sale';
$args['amt'] = $params['amount'];
$args['currencyCode'] = $params['currencyID'];
$args['invnum'] = $params['invoiceID'];
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 » System Settings » Payment Processors.');
- }
- }
if ($this->_paymentProcessor['payment_processor_type_id'] != CRM_Utils_Array::key('PayPal_Standard', $paymentProcessorType)) {
if (empty($this->_paymentProcessor['signature'])) {
$error[] = ts('Password is not set in the Administer » System Settings » Payment Processors.');
}
}
+ if (empty($this->_paymentProcessor['user_name'])) {
+ $error[] = ts('User Name is not set in the Administer » System Settings » Payment Processors.');
+ }
if (!empty($error)) {
return implode('<p>', $error);
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
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
/**
* 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.
*/
$result = array();
while (strlen($str)) {
- // postion of key
+ // position of key
$keyPos = strpos($str, '=');
// position of value
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;
+ }
+
}