From 21843539050b1831e0a20302d5da70bf6b933dcb Mon Sep 17 00:00:00 2001 From: Seamus Lee Date: Sat, 14 Sep 2019 07:21:48 +1000 Subject: [PATCH] dev/core#1249 Allow IPNs to be validated even if the supplied contact id is wrong by checking for contact ids in other objects Rework the checking code to get contribution first then try using the contact_id from the contribution and log if it is different and error if we can't find the linked contact_id Rationalise the validation as contact_id in the contribution should always be there and do not worry about checking is_deleted or is_deceased Ensure we load the contact object --- CRM/Core/Payment/BaseIPN.php | 33 +++++++++++++----- .../CRM/Core/Payment/PayPalIPNTest.php | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CRM/Core/Payment/BaseIPN.php b/CRM/Core/Payment/BaseIPN.php index 9e7ed420ba..aa61a62480 100644 --- a/CRM/Core/Payment/BaseIPN.php +++ b/CRM/Core/Payment/BaseIPN.php @@ -103,15 +103,7 @@ class CRM_Core_Payment_BaseIPN { */ public function validateData(&$input, &$ids, &$objects, $required = TRUE, $paymentProcessorID = NULL) { - // make sure contact exists and is valid - $contact = new CRM_Contact_BAO_Contact(); - $contact->id = $ids['contact']; - if (!$contact->find(TRUE)) { - CRM_Core_Error::debug_log_message("Could not find contact record: {$ids['contact']} in IPN request: " . print_r($input, TRUE)); - echo "Failure: Could not find contact record: {$ids['contact']}

"; - return FALSE; - } - + // Check if the contribution exists // make sure contribution exists and is valid $contribution = new CRM_Contribute_BAO_Contribution(); $contribution->id = $ids['contribution']; @@ -120,6 +112,29 @@ class CRM_Core_Payment_BaseIPN { echo "Failure: Could not find contribution record for {$contribution->id}

"; return FALSE; } + + // make sure contact exists and is valid + // use the contact id from the contribution record as the id in the IPN may not be valid anymore. + $contact = new CRM_Contact_BAO_Contact(); + $contact->id = $contribution->contact_id; + $contact->find(TRUE); + if ($contact->id != $ids['contact']) { + // If the ids do not match then it is possible the contact id in the IPN has been merged into another contact which is why we use the contact_id from the contribution + CRM_Core_Error::debug_log_message("Contact ID in IPN {$ids['contact']} not found but contact_id found in contribution {$contribution->contact_id} used instead"); + echo "WARNING: Could not find contact record: {$ids['contact']}

"; + $ids['contact'] = $contribution->contact_id; + } + + if (!empty($ids['contributionRecur'])) { + $contributionRecur = new CRM_Contribute_BAO_ContributionRecur(); + $contributionRecur->id = $ids['contributionRecur']; + if (!$contributionRecur->find(TRUE)) { + CRM_Core_Error::debug_log_message("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE)); + echo "Failure: Could not find contribution recur record: {$ids['ContributionRecur']}

"; + return FALSE; + } + } + $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date); $contribution->receipt_date = CRM_Utils_Date::isoToMysql($contribution->receipt_date); diff --git a/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php b/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php index 71d93aa804..736415c453 100644 --- a/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php +++ b/tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php @@ -333,6 +333,40 @@ class CRM_Core_Payment_PayPalIPNTest extends CiviUnitTestCase { $this->assertEquals('test12345', $contribution['values'][0]['custom_' . $this->_customFieldID]); } + /** + * Allow IPNs to validate when the supplied contact_id has been deleted from the database but there is a valid contact id in the contribution recur object or contribution object + */ + public function testPayPalIPNSuccessDeletedContact() { + $contactTobeDeleted = $this->individualCreate(); + $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 = [ + '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); + $contribution = $this->callAPISuccess('contribution', 'get', ['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']); + $payPalIPNParams = $this->getPaypalTransaction(); + $payPalIPNParams['contactID'] = $contactTobeDeleted; + $this->callAPISuccess('Contact', 'delete', ['id' => $contactTobeDeleted, 'skip_undelete' => 1]); + global $_REQUEST; + $_REQUEST = ['q' => CRM_Utils_System::url('civicrm/payment/ipn/' . $this->_paymentProcessorID)] + $payPalIPNParams; + // Now process the IPN noting that the contact id that was supplied with the IPN has been deleted but there is still a valid one on the contribution id + $payment = CRM_Core_Payment::handlePaymentMethod('PaymentNotification', ['processor_id' => $this->_paymentProcessorID]); + + $contribution = $this->callAPISuccess('contribution', 'get', ['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']); + } + /** * Store Custom data passed in from the PayPalIPN in a custom field */ -- 2.25.1