dev/core#1249 Allow IPNs to be validated even if the supplied contact id is wrong...
authorSeamus Lee <seamuslee001@gmail.com>
Fri, 13 Sep 2019 21:21:48 +0000 (07:21 +1000)
committerSeamus Lee <seamuslee001@gmail.com>
Wed, 16 Oct 2019 02:27:36 +0000 (13:27 +1100)
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
tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php

index 9e7ed420ba3c9330dd5a2255e866684838df229b..aa61a624802304804ab1bc3d20db7686574e9f6a 100644 (file)
@@ -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']}<p>";
-      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}<p>";
       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']}<p>";
+      $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']}<p>";
+        return FALSE;
+      }
+    }
+
     $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date);
     $contribution->receipt_date = CRM_Utils_Date::isoToMysql($contribution->receipt_date);
 
index 71d93aa8049eea794596ccc7bb0ceca018fbf80c..736415c45325575af1939b00b49ee9139e1215f5 100644 (file)
@@ -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
    */