diff --git a/CRM/Core/Payment.php b/CRM/Core/Payment.php index 63ce4d9b5a31..cf31189d384d 100644 --- a/CRM/Core/Payment.php +++ b/CRM/Core/Payment.php @@ -1393,6 +1393,9 @@ public static function handlePaymentMethod($method, $params = array()) { $extension_instance_found = TRUE; } + // Call IPN alterIPNData hook to allow for custom processing of IPN data. + $IPNParams = array_merge($_GET, $_REQUEST); + CRM_Utils_Hook::postIPNProcess($IPNParams); if (!$extension_instance_found) { $message = "No extension instances of the '%1' payment processor were found.
" . "%2 method is unsupported in legacy payment processors."; diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index 92989a902534..e00b3f2f93e1 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -2473,4 +2473,17 @@ public static function alterMailingRecipients(&$mailingObject, &$criteria, $cont ); } + /** + * ALlow Extensions to custom process IPN hook data such as sending Google Analyitcs information based on the IPN + * @param array $IPNData - Array of IPN Data + * @return mixed + */ + public static function postIPNProcess(&$IPNData) { + return self::singleton()->invoke(array('IPNData'), + $IPNData, self::$_nullObject, self::$_nullObject, + self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_postIPNProcess' + ); + } + } diff --git a/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php b/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php index 9de32d4ea3a8..c9862c05ec5e 100644 --- a/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php +++ b/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php @@ -37,6 +37,7 @@ class CRM_Core_Payment_PayPalIPNTest extends CiviUnitTestCase { protected $_contributionRecurID; protected $_contributionPageID; protected $_paymentProcessorID; + protected $_customFieldID; /** * IDs of entities created to support the tests. * @@ -237,6 +238,7 @@ public function getPaypalTransaction() { 'first_name' => 'Robert', 'txn_id' => '8XA571746W2698126', 'residence_country' => 'US', + 'custom' => json_encode(['cgid' => 'test12345']), ); } @@ -249,4 +251,69 @@ public function getPaypalRecurSubsequentTransaction() { return array_merge($this->getPaypalRecurTransaction(), array('txn_id' => 'secondone')); } + /** + * Test IPN response updates contribution and invoice is attached in mail reciept + * Test also AlterIPNData intercepts at the right point and allows for custom processing + * The scenario is that a pending contribution exists and the IPN call will update it to completed. + * And also if Tax and Invoicing is enabled, this unit test ensure that invoice pdf is attached with email recipet + */ + public function testhookAlterIPNDataOnIPNPaymentSuccess() { + + $pendingStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'); + $completedStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + $params = array( + 'payment_processor_id' => $this->_paymentProcessorID, + 'contact_id' => $this->_contactID, + 'trxn_id' => NULL, + 'invoice_id' => $this->_invoiceID, + 'contribution_status_id' => $pendingStatusID, + 'is_email_receipt' => TRUE, + ); + $this->_contributionID = $this->contributionCreate($params); + $this->createCustomField(); + $contribution = $this->callAPISuccess('contribution', 'get', array('id' => $this->_contributionID, 'sequential' => 1)); + // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status + $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']); + $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']); + $this->hookClass->setHook('civicrm_postIPNProcess', array($this, 'hookCiviCRMAlterIPNData')); + global $_REQUEST; + $_REQUEST = array('q' => CRM_Utils_System::url('civicrm/payment/ipn/' . $this->_paymentProcessorID)) + $this->getPaypalTransaction(); + + $mut = new CiviMailUtils($this, TRUE); + $payment = CRM_Core_Payment::handlePaymentMethod('PaymentNotification', ['processor_id' => $this->_paymentProcessorID]); + + $contribution = $this->callAPISuccess('contribution', 'get', array('id' => $this->_contributionID, 'sequential' => 1)); + // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status + $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']); + $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']); + $this->assertEquals('test12345', $contribution['values'][0]['custom_' . $this->_customFieldID]); + } + + /** + * Store Custom data passed in from the PayPalIPN in a custom field + */ + public function hookCiviCRMAlterIPNData($data) { + if (!empty($data['custom'])) { + $customData = json_decode($data['custom'], TRUE); + $customField = $this->callAPISuccess('custom_field', 'get', ['label' => 'TestCustomFieldIPNHook']); + $this->callAPISuccess('contribution', 'create', ['id' => $this->_contributionID, 'custom_' . $customField['id'] => $customData['cgid']]); + } + } + + /** + * @return array + */ + protected function createCustomField() { + $customGroup = $this->customGroupCreate(array('extends' => 'Contribution')); + $fields = array( + 'label' => 'TestCustomFieldIPNHook', + 'data_type' => 'String', + 'html_type' => 'Text', + 'custom_group_id' => $customGroup['id'], + ); + $field = CRM_Core_BAO_CustomField::create($fields); + $this->_customFieldID = $field->id; + return $customGroup; + } + }