Merge pull request #18934 from eileenmcnaughton/aip
[civicrm-core.git] / CRM / Core / Payment / AuthorizeNetIPN.php
index d72dc82fa22d430397b5f4430701bd729dc6d0d3..04042b587a8adcb7909c498f088668bc398eb0be 100644 (file)
@@ -30,121 +30,100 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
   }
 
   /**
-   * @param string $component
+   * Main IPN processing function.
    *
    * @return bool|void
+   *
+   * @throws \CiviCRM_API3_Exception
    */
-  public function main($component = 'contribute') {
+  public function main() {
     try {
       //we only get invoice num as a key player from payment gateway response.
       //for ARB we get x_subscription_id and x_subscription_paynum
       $x_subscription_id = $this->retrieve('x_subscription_id', 'String');
+      if (!$x_subscription_id) {
+        // Presence of the id means it is approved.
+        return TRUE;
+      }
       $ids = $objects = $input = [];
 
-      if ($x_subscription_id) {
-        // Presence of the id means it is approved.
-        $input['component'] = $component;
-
-        // load post vars in $input
-        $this->getInput($input, $ids);
-
-        // load post ids in $ids
-        $this->getIDs($ids, $input);
-
-        // Attempt to get payment processor ID from URL
-        if (!empty($this->_inputParameters['processor_id'])) {
-          $paymentProcessorID = $this->_inputParameters['processor_id'];
-        }
-        else {
-          // This is an unreliable method as there could be more than one instance.
-          // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment
-          // processor id & the handleNotification function (which should call the completetransaction api & by-pass this
-          // entirely). The only thing the IPN class should really do is extract data from the request, validate it
-          // & call completetransaction or call fail? (which may not exist yet).
-          Civi::log()->warning('Unreliable method used to get payment_processor_id for AuthNet IPN - this will cause problems if you have more than one instance');
-          $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType',
-            'AuthNet', 'id', 'name'
-          );
-          $paymentProcessorID = (int) civicrm_api3('PaymentProcessor', 'getvalue', [
-            'is_test' => 0,
-            'options' => ['limit' => 1],
-            'payment_processor_type_id' => $paymentProcessorTypeID,
-            'return' => 'id',
-          ]);
-        }
-
-        // Check if the contribution exists
-        // make sure contribution exists and is valid
+      $input['component'] = 'contribute';
+
+      // load post vars in $input
+      $this->getInput($input, $ids);
+
+      // load post ids in $ids
+      $this->getIDs($ids, $input);
+      $paymentProcessorID = $this->getPaymentProcessorID();
+
+      // Check if the contribution exists
+      // make sure contribution exists and is valid
+      $contribution = new CRM_Contribute_BAO_Contribution();
+      $contribution->id = $ids['contribution'];
+      if (!$contribution->find(TRUE)) {
+        throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]);
+      }
+      $ids['contributionPage'] = $contribution->contribution_page_id;
+
+      // 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;
+      }
+
+      $contributionRecur = new CRM_Contribute_BAO_ContributionRecur();
+      $contributionRecur->id = $ids['contributionRecur'];
+      if (!$contributionRecur->find(TRUE)) {
+        throw new CRM_Core_Exception("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE));
+      }
+
+      $objects['contact'] = &$contact;
+      $objects['contribution'] = &$contribution;
+
+      $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
+
+      // check if first contribution is completed, else complete first contribution
+      $first = TRUE;
+      if ($objects['contribution']->contribution_status_id == 1) {
+        $first = FALSE;
+        //load new contribution object if required.
+        // create a contribution and then get it processed
         $contribution = new CRM_Contribute_BAO_Contribution();
-        $contribution->id = $ids['contribution'];
-        if (!$contribution->find(TRUE)) {
-          throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]);
-        }
-        $ids['contributionPage'] = $contribution->contribution_page_id;
-
-        // 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;
-          }
-        }
-
-        $objects['contact'] = &$contact;
-        $objects['contribution'] = &$contribution;
+        $contribution->contact_id = $ids['contact'];
+        $contribution->financial_type_id = $objects['contributionType']->id;
+        $contribution->contribution_page_id = $ids['contributionPage'];
+        $contribution->contribution_recur_id = $ids['contributionRecur'];
+        $contribution->receive_date = $input['receive_date'];
+        $contribution->currency = $objects['contribution']->currency;
+        $contribution->amount_level = $objects['contribution']->amount_level;
+        $contribution->address_id = $objects['contribution']->address_id;
+        $contribution->campaign_id = $objects['contribution']->campaign_id;
 
-        $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
-
-        if (!empty($ids['paymentProcessor']) && $objects['contributionRecur']->payment_processor_id != $ids['paymentProcessor']) {
-          Civi::log()->warning('Payment Processor does not match the recurring processor id.', ['civi.tag' => 'deprecated']);
-        }
-
-        if ($component == 'contribute' && $ids['contributionRecur']) {
-          // check if first contribution is completed, else complete first contribution
-          $first = TRUE;
-          if ($objects['contribution']->contribution_status_id == 1) {
-            $first = FALSE;
-            //load new contribution object if required.
-            // create a contribution and then get it processed
-            $contribution = new CRM_Contribute_BAO_Contribution();
-            $contribution->contact_id = $ids['contact'];
-            $contribution->financial_type_id = $objects['contributionType']->id;
-            $contribution->contribution_page_id = $ids['contributionPage'];
-            $contribution->contribution_recur_id = $ids['contributionRecur'];
-            $contribution->receive_date = $input['receive_date'];
-            $contribution->currency = $objects['contribution']->currency;
-            $contribution->amount_level = $objects['contribution']->amount_level;
-            $contribution->address_id = $objects['contribution']->address_id;
-            $contribution->campaign_id = $objects['contribution']->campaign_id;
-            $contribution->_relatedObjects = $objects['contribution']->_relatedObjects;
-
-            $objects['contribution'] = &$contribution;
-          }
-          $input['payment_processor_id'] = $paymentProcessorID;
-          return $this->recur($input, [
-            'related_contact' => $ids['related_contact'] ?? NULL,
-            'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
-            'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
-            'membership' => $ids['membership'] ?? NULL,
-            'contact' => $ids['contact'] ?? NULL,
-            'contributionPage' => $ids['contributionPage'] ?? NULL,
-          ], $objects, $first);
-        }
+        $objects['contribution'] = &$contribution;
       }
+      $input['payment_processor_id'] = $paymentProcessorID;
+      $isFirstOrLastRecurringPayment = $this->recur($input, [
+        'related_contact' => $ids['related_contact'] ?? NULL,
+        'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
+        'contributionRecur' => $contributionRecur->id,
+      ], $contributionRecur, $objects['contribution'], $first);
+
+      if ($isFirstOrLastRecurringPayment) {
+        //send recurring Notification email for user
+        CRM_Contribute_BAO_ContributionPage::recurringNotify(TRUE,
+          $ids['contact'],
+          $ids['contributionPage'],
+          $contributionRecur,
+          (bool) $this->getMembershipID($contribution->id, $contributionRecur->id)
+        );
+      }
+
       return TRUE;
     }
     catch (CRM_Core_Exception $e) {
@@ -156,15 +135,15 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
   /**
    * @param array $input
    * @param array $ids
-   * @param array $objects
-   * @param $first
+   * @param \CRM_Contribute_BAO_ContributionRecur $recur
+   * @param \CRM_Contribute_BAO_Contribution $contribution
+   * @param bool $first
    *
    * @return bool
    * @throws \CRM_Core_Exception
    * @throws \CiviCRM_API3_Exception
    */
-  public function recur($input, $ids, $objects, $first) {
-    $recur = &$objects['contributionRecur'];
+  public function recur($input, $ids, $recur, $contribution, $first) {
 
     // do a subscription check
     if ($recur->processor_id != $input['subscription_id']) {
@@ -175,9 +154,9 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
 
     $now = date('YmdHis');
 
-    $objects['contribution']->invoice_id = md5(uniqid(rand(), TRUE));
-    $objects['contribution']->total_amount = $input['amount'];
-    $objects['contribution']->trxn_id = $input['trxn_id'];
+    $contribution->invoice_id = md5(uniqid(rand(), TRUE));
+    $contribution->total_amount = $input['amount'];
+    $contribution->trxn_id = $input['trxn_id'];
 
     $isFirstOrLastRecurringPayment = FALSE;
     if ($input['response_code'] == 1) {
@@ -213,27 +192,18 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
       // so we just fix the recurring contribution and not change any of
       // the existing contributions
       // CRM-9036
-      return TRUE;
+      return FALSE;
     }
 
     // check if contribution is already completed, if so we ignore this ipn
-    if ($objects['contribution']->contribution_status_id == 1) {
+    if ($contribution->contribution_status_id == 1) {
       CRM_Core_Error::debug_log_message("Returning since contribution has already been handled.");
       echo 'Success: Contribution has already been handled<p>';
-      return TRUE;
-    }
-
-    CRM_Contribute_BAO_Contribution::completeOrder($input, $ids, $objects['contribution']);
-
-    // Only Authorize.net does this so it is on the a.net class. If there is a need for other processors
-    // to do this we should make it available via the api, e.g as a parameter, changing the nuance
-    // from isSentReceipt to an array of which receipts to send.
-    // Note that there is site-by-site opinions on which notifications are good to send.
-    if ($isFirstOrLastRecurringPayment) {
-      CRM_Contribute_BAO_ContributionRecur::sendRecurringStartOrEndNotification($ids, $recur,
-        $isFirstOrLastRecurringPayment);
+      return FALSE;
     }
 
+    CRM_Contribute_BAO_Contribution::completeOrder($input, $ids, $contribution);
+    return $isFirstOrLastRecurringPayment;
   }
 
   /**
@@ -288,33 +258,12 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
    *
    * @throws \CRM_Core_Exception
    */
-  public function getIDs(&$ids, &$input) {
-    $ids['contact'] = $this->retrieve('x_cust_id', 'Integer', FALSE, 0);
+  public function getIDs(&$ids, $input) {
+    $ids['contact'] = (int) $this->retrieve('x_cust_id', 'Integer', FALSE, 0);
     $ids['contribution'] = (int) $this->retrieve('x_invoice_num', 'Integer');
-
-    // joining with contribution table for extra checks
-    $sql = "
-    SELECT cr.id, cr.contact_id
-      FROM civicrm_contribution_recur cr
-INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id
-     WHERE cr.processor_id = '{$input['subscription_id']}' AND
-           (cr.contact_id = {$ids['contact']} OR co.id = {$ids['contribution']})
-     LIMIT 1";
-    $contRecur = CRM_Core_DAO::executeQuery($sql);
-    $contRecur->fetch();
-    $ids['contributionRecur'] = (int) $contRecur->id;
-    if ($ids['contact'] != $contRecur->contact_id) {
-      $message = ts("Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.", [1 => $ids['contact'], 2 => $contRecur->contact_id]);
-      CRM_Core_Error::debug_log_message($message);
-      $ids['contact'] = $contRecur->contact_id;
-    }
-    if (!$ids['contributionRecur']) {
-      $message = ts("Could not find contributionRecur id");
-      $log = new CRM_Utils_SystemLogger();
-      $log->error('payment_notification', ['message' => $message, 'ids' => $ids, 'input' => $input]);
-      throw new CRM_Core_Exception($message);
-    }
-    $ids['membership'] = $this->getMembershipID($ids['contribution'], $ids['contributionRecur']);
+    $contributionRecur = $this->getContributionRecurObject($input['subscription_id'], $ids['contact'], $ids['contribution']);
+    $ids['contributionRecur'] = (int) $contributionRecur->id;
+    $ids['contact'] = $contributionRecur->contact_id;
   }
 
   /**
@@ -361,4 +310,64 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr
     return CRM_Core_DAO::singleValueQuery($sql);
   }
 
+  /**
+   * Get the recurring contribution object.
+   *
+   * @param string $processorID
+   * @param int $contactID
+   * @param int $contributionID
+   *
+   * @return \CRM_Core_DAO|\DB_Error|object
+   * @throws \CRM_Core_Exception
+   */
+  protected function getContributionRecurObject(string $processorID, int $contactID, int $contributionID) {
+    // joining with contribution table for extra checks
+    $sql = "
+    SELECT cr.id, cr.contact_id
+      FROM civicrm_contribution_recur cr
+INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id
+     WHERE cr.processor_id = '{$processorID}' AND
+           (cr.contact_id = $contactID OR co.id = $contributionID)
+     LIMIT 1";
+    $contRecur = CRM_Core_DAO::executeQuery($sql);
+    if (!$contRecur->fetch()) {
+      throw new CRM_Core_Exception('Could not find contributionRecur id');
+    }
+    if ($contactID != $contRecur->contact_id) {
+      $message = ts("Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.", [1 => $ids['contact'], 2 => $contRecur->contact_id]);
+      CRM_Core_Error::debug_log_message($message);
+    }
+    return $contRecur;
+  }
+
+  /**
+   * Get the payment processor id.
+   *
+   * @return int
+   *
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
+   */
+  protected function getPaymentProcessorID(): int {
+    // Attempt to get payment processor ID from URL
+    if (!empty($this->_inputParameters['processor_id'])) {
+      return (int) $this->_inputParameters['processor_id'];
+    }
+    // This is an unreliable method as there could be more than one instance.
+    // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment
+    // processor id & the handleNotification function (which should call the completetransaction api & by-pass this
+    // entirely). The only thing the IPN class should really do is extract data from the request, validate it
+    // & call completetransaction or call fail? (which may not exist yet).
+    Civi::log()->warning('Unreliable method used to get payment_processor_id for AuthNet IPN - this will cause problems if you have more than one instance');
+    $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType',
+      'AuthNet', 'id', 'name'
+    );
+    return (int) civicrm_api3('PaymentProcessor', 'getvalue', [
+      'is_test' => 0,
+      'options' => ['limit' => 1],
+      'payment_processor_type_id' => $paymentProcessorTypeID,
+      'return' => 'id',
+    ]);
+  }
+
 }