3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | This file is a part of CiviCRM. |
8 | CiviCRM is free software; you can copy, modify, and distribute it |
9 | under the terms of the GNU Affero General Public License |
10 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
12 | CiviCRM is distributed in the hope that it will be useful, but |
13 | WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
15 | See the GNU Affero General Public License for more details. |
17 | You should have received a copy of the GNU Affero General Public |
18 | License and the CiviCRM Licensing Exception along |
19 | with this program; if not, contact CiviCRM LLC |
20 | at info[AT]civicrm[DOT]org. If you have questions about the |
21 | GNU Affero General Public License or the licensing of CiviCRM, |
22 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
23 +--------------------------------------------------------------------+
28 * PxPay Functionality Copyright (C) 2008 Lucas Baker, Logistic Information Systems Limited (Logis)
29 * PxAccess Functionality Copyright (C) 2008 Eileen McNaughton
30 * Licensed to CiviCRM under the Academic Free License version 3.0.
32 * Grateful acknowledgements go to Donald Lobo for invaluable assistance
33 * in creating this payment processor module
37 * Class CRM_Core_Payment_PaymentExpressIPN
39 class CRM_Core_Payment_PaymentExpressIPN
extends CRM_Core_Payment_BaseIPN
{
42 * We only need one instance of this object. So we use the singleton
43 * pattern and cache the instance in this variable
48 static private $_singleton = NULL;
51 * mode of operation: live or test
55 protected $_mode = NULL;
65 static function retrieve($name, $type, $object, $abort = TRUE) {
66 $value = CRM_Utils_Array
::value($name, $object);
67 if ($abort && $value === NULL) {
68 CRM_Core_Error
::debug_log_message("Could not find an entry for $name");
69 echo "Failure: Missing Parameter - " . $name . "<p>";
74 if (!CRM_Utils_Type
::validate($value, $type)) {
75 CRM_Core_Error
::debug_log_message("Could not find a valid entry for $name");
76 echo "Failure: Invalid Parameter<p>";
87 * @param string $mode the mode of operation: live or test
89 * @param $paymentProcessor
91 * @return \CRM_Core_Payment_PaymentExpressIPN
93 function __construct($mode, &$paymentProcessor) {
94 parent
::__construct();
97 $this->_paymentProcessor
= $paymentProcessor;
101 * singleton function used to manage this object
103 * @param string $mode the mode of operation: live or test
105 * @param $paymentProcessor
106 * @param null $paymentForm
112 static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) {
113 if (self
::$_singleton === NULL) {
114 self
::$_singleton = new CRM_Core_Payment_PaymentExpressIPN($mode, $paymentProcessor);
116 return self
::$_singleton;
120 * The function gets called when a new order takes place.
123 * @param array $privateData contains the name value pair of <merchant-private-data>
127 * @param $transactionReference
129 * @internal param \xml $dataRoot response send by google in xml format
132 function newOrderNotify($success, $privateData, $component, $amount, $transactionReference) {
133 $ids = $input = $params = array();
135 $input['component'] = strtolower($component);
137 $ids['contact'] = self
::retrieve('contactID', 'Integer', $privateData, TRUE);
138 $ids['contribution'] = self
::retrieve('contributionID', 'Integer', $privateData, TRUE);
140 if ($input['component'] == "event") {
141 $ids['event'] = self
::retrieve('eventID', 'Integer', $privateData, TRUE);
142 $ids['participant'] = self
::retrieve('participantID', 'Integer', $privateData, TRUE);
143 $ids['membership'] = NULL;
146 $ids['membership'] = self
::retrieve('membershipID', 'Integer', $privateData, FALSE);
148 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
150 $paymentProcessorID = CRM_Core_DAO
::getFieldValue('CRM_Financial_DAO_PaymentProcessorType',
151 'PayPal_Express', 'id', 'name'
154 if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
158 // make sure the invoice is valid and matches what we have in the contribution record
159 $input['invoice'] = $privateData['invoiceID'];
160 $input['newInvoice'] = $transactionReference;
161 $contribution = &$objects['contribution'];
162 $input['trxn_id'] = $transactionReference;
164 if ($contribution->invoice_id
!= $input['invoice']) {
165 CRM_Core_Error
::debug_log_message("Invoice values dont match between database and IPN request");
166 echo "Failure: Invoice values dont match between database and IPN request<p>";
170 // lets replace invoice-id with Payment Processor -number because thats what is common and unique
171 // in subsequent calls or notifications sent by google.
172 $contribution->invoice_id
= $input['newInvoice'];
174 $input['amount'] = $amount;
176 if ($contribution->total_amount
!= $input['amount']) {
177 CRM_Core_Error
::debug_log_message("Amount values dont match between database and IPN request");
178 echo "Failure: Amount values dont match between database and IPN request. " . $contribution->total_amount
. "/" . $input['amount'] . "<p>";
182 $transaction = new CRM_Core_Transaction();
184 // check if contribution is already completed, if so we ignore this ipn
186 if ($contribution->contribution_status_id
== 1) {
187 CRM_Core_Error
::debug_log_message("returning since contribution has already been handled");
188 echo "Success: Contribution has already been handled<p>";
192 /* Since trxn_id hasn't got any use here,
193 * lets make use of it by passing the eventID/membershipTypeID to next level.
194 * And change trxn_id to the payment processor reference before finishing db update */
197 $contribution->trxn_id
= $ids['event'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['participant'];
200 $contribution->trxn_id
= $ids['membership'];
203 $this->completeTransaction($input, $ids, $objects, $transaction);
210 * The function returns the component(Event/Contribute..)and whether it is Test or not
212 * @param array $privateData contains the name-value pairs of transaction related data
213 * @param int $orderNo <order-total> send by google
215 * @return array context of this call (test, component, payment processor id)
218 static function getContext($privateData, $orderNo) {
223 $contributionID = $privateData['contributionID'];
224 $contribution = new CRM_Contribute_DAO_Contribution();
225 $contribution->id
= $contributionID;
227 if (!$contribution->find(TRUE)) {
228 CRM_Core_Error
::debug_log_message("Could not find contribution record: $contributionID");
229 echo "Failure: Could not find contribution record for $contributionID<p>";
233 if (stristr($contribution->source
, 'Online Contribution')) {
234 $component = 'contribute';
236 elseif (stristr($contribution->source
, 'Online Event Registration')) {
237 $component = 'event';
239 $isTest = $contribution->is_test
;
241 $duplicateTransaction = 0;
242 if ($contribution->contribution_status_id
== 1) {
243 //contribution already handled. (some processors do two notifications so this could be valid)
244 $duplicateTransaction = 1;
247 if ($component == 'contribute') {
248 if (!$contribution->contribution_page_id
) {
249 CRM_Core_Error
::debug_log_message("Could not find contribution page for contribution record: $contributionID");
250 echo "Failure: Could not find contribution page for contribution record: $contributionID<p>";
256 $eventID = $privateData['eventID'];
259 CRM_Core_Error
::debug_log_message("Could not find event ID");
260 echo "Failure: Could not find eventID<p>";
264 // we are in event mode
265 // make sure event exists and is valid
266 $event = new CRM_Event_DAO_Event();
267 $event->id
= $eventID;
268 if (!$event->find(TRUE)) {
269 CRM_Core_Error
::debug_log_message("Could not find event: $eventID");
270 echo "Failure: Could not find event: $eventID<p>";
275 return array($isTest, $component, $duplicateTransaction);
279 * This method is handles the response that will be invoked by the
280 * notification or request sent by the payment processor.
281 *hex string from paymentexpress is passed to this function as hex string. Code based on googleIPN
282 * mac_key is only passed if the processor is pxaccess as it is used for decryption
283 * $dps_method is either pxaccess or pxpay
285 static function main($dps_method, $rawPostData, $dps_url, $dps_user, $dps_key, $mac_key) {
287 $config = CRM_Core_Config
::singleton();
288 define('RESPONSE_HANDLER_LOG_FILE', $config->uploadDir
. 'CiviCRM.PaymentExpress.log');
291 if (!$message_log = fopen(RESPONSE_HANDLER_LOG_FILE
, "a")) {
292 error_func("Cannot open " . RESPONSE_HANDLER_LOG_FILE
. " file.\n", 0);
296 if ($dps_method == "pxpay") {
297 $processResponse = CRM_Core_Payment_PaymentExpressUtils
::_valueXml(array(
298 'PxPayUserId' => $dps_user,
299 'PxPayKey' => $dps_key,
300 'Response' => $_GET['result'],
302 $processResponse = CRM_Core_Payment_PaymentExpressUtils
::_valueXml('ProcessResponse', $processResponse);
304 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
308 // Send the XML-formatted validation request to DPS so that we can receive a decrypted XML response which contains the transaction results
309 $curl = CRM_Core_Payment_PaymentExpressUtils
::_initCURL($processResponse, $dps_url);
311 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
315 if ($response = curl_exec($curl)) {
316 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
321 // Assign the returned XML values to variables
322 $valid = CRM_Core_Payment_PaymentExpressUtils
::_xmlAttribute($response, 'valid');
323 $success = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, 'Success');
324 $txnId = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, 'TxnId');
325 $responseText = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, 'ResponseText');
326 $authCode = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, 'AuthCode');
327 $DPStxnRef = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, 'DpsTxnRef');
328 $qfKey = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, "TxnData1");
329 $privateData = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, "TxnData2");
330 list($component,$paymentProcessorID,) =explode(',', CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, "TxnData3"));
331 $amount = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, "AmountSettlement");
332 $merchantReference = CRM_Core_Payment_PaymentExpressUtils
::_xmlElement($response, "MerchantReference");
335 // calling DPS failed
336 CRM_Core_Error
::fatal(ts('Unable to establish connection to the payment gateway to verify transaction response.'));
340 elseif ($dps_method == "pxaccess") {
342 require_once ('PaymentExpress/pxaccess.inc.php');
344 $pxaccess = new PxAccess($dps_url, $dps_user, $dps_key, $mac_key);
345 #getResponse method in PxAccess object returns PxPayResponse object
346 #which encapsulates all the response data
347 $rsp = $pxaccess->getResponse($rawPostData);
349 $qfKey = $rsp->getTxnData1();
350 $privateData = $rsp->getTxnData2();
351 list($component,$paymentProcessorID) = explode(',',$rsp->getTxnData3());
352 $success = $rsp->getSuccess();
353 $authCode = $rsp->getAuthCode();
354 $DPStxnRef = $rsp->getDpsTxnRef();
355 $amount = $rsp->getAmountSettlement();
356 $MerchantReference = $rsp->getMerchantReference();
359 $privateData = $privateData ? self
::stringToArray($privateData) : '';
361 // Record the current count in array, before we start adding things (for later checks)
362 $countPrivateData = count($privateData);
364 // Private Data consists of : a=contactID, b=contributionID,c=contributionTypeID,d=invoiceID,e=membershipID,f=participantID,g=eventID
365 $privateData['contactID'] = $privateData['a'];
366 $privateData['contributionID'] = $privateData['b'];
367 $privateData['contributionTypeID'] = $privateData['c'];
368 $privateData['invoiceID'] = $privateData['d'];
370 if ($component == "event") {
371 $privateData['participantID'] = $privateData['f'];
372 $privateData['eventID'] = $privateData['g'];
374 elseif ($component == "contribute") {
376 if ($countPrivateData == 5) {
377 $privateData["membershipID"] = $privateData['e'];
381 $transactionReference = $authCode . "-" . $DPStxnRef;
383 list($mode, $component, $duplicateTransaction) = self
::getContext($privateData, $transactionReference);
384 $mode = $mode ?
'test' : 'live';
387 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor
::getPayment($paymentProcessorID,
391 $ipn = self
::singleton($mode, $component, $paymentProcessor);
394 //Check status and take appropriate action
397 if ($duplicateTransaction == 0) {
398 $ipn->newOrderNotify($success, $privateData, $component, $amount, $transactionReference);
401 if ($component == "event") {
402 $finalURL = CRM_Utils_System
::url('civicrm/event/register',
403 "_qf_ThankYou_display=1&qfKey=$qfKey",
407 elseif ($component == "contribute") {
408 $finalURL = CRM_Utils_System
::url('civicrm/contribute/transact',
409 "_qf_ThankYou_display=1&qfKey=$qfKey",
414 CRM_Utils_System
::redirect($finalURL);
418 if ($component == "event") {
419 $finalURL = CRM_Utils_System
::url('civicrm/event/confirm',
420 "reset=1&cc=fail&participantId=$privateData[participantID]",
424 elseif ($component == "contribute") {
425 $finalURL = CRM_Utils_System
::url('civicrm/contribute/transact',
426 "_qf_Main_display=1&cancel=1&qfKey=$qfKey",
431 CRM_Utils_System
::redirect($finalURL);
436 * Converts the comma separated name-value pairs in <TxnData2>
437 * to an array of values.
439 static function stringToArray($str) {
440 $vars = $labels = array();
441 $labels = explode(',', $str);
442 foreach ($labels as $label) {
443 $terms = explode('=', $label);
444 $vars[$terms[0]] = $terms[1];