From 86b1c2f3331ea7e282995e92c608f36b8df2a76f Mon Sep 17 00:00:00 2001
From: Eileen McNaughton <emcnaughton@wikimedia.org>
Date: Thu, 22 Jun 2023 21:54:35 -0700
Subject: [PATCH] Switch to Payment.create & repeattransaction in Authorize.net

---
 CRM/Contribute/BAO/Contribution.php           |   1 +
 CRM/Core/Payment/AuthorizeNetIPN.php          | 100 +++++++++++++-----
 CRM/Financial/BAO/Payment.php                 |   1 +
 .../CRM/Core/Payment/AuthorizeNetIPNTest.php  |   2 +-
 4 files changed, 76 insertions(+), 28 deletions(-)

diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php
index d01cbcc9ee5a..6b7e21327687 100644
--- a/CRM/Contribute/BAO/Contribution.php
+++ b/CRM/Contribute/BAO/Contribution.php
@@ -3816,6 +3816,7 @@ public static function isSingleLineItem($id) {
    */
   public static function completeOrder($input, $recurringContributionID, $contributionID, $isPostPaymentCreate = FALSE) {
     if (!$contributionID) {
+      CRM_Core_Error::deprecatedFunctionWarning('v3api Contribution.repeattransaction. This handling will be removed around 5.70 (calling this function directly has never been supported outside core anyway)');
       return self::repeatTransaction($input, $recurringContributionID);
     }
     $transaction = new CRM_Core_Transaction();
diff --git a/CRM/Core/Payment/AuthorizeNetIPN.php b/CRM/Core/Payment/AuthorizeNetIPN.php
index 2507fed8dbbd..6cd7b694a5a6 100644
--- a/CRM/Core/Payment/AuthorizeNetIPN.php
+++ b/CRM/Core/Payment/AuthorizeNetIPN.php
@@ -33,6 +33,16 @@ public function __construct($inputData) {
     parent::__construct();
   }
 
+  /**
+   * @var string
+   */
+  protected $transactionID;
+
+  /**
+   * @var string
+   */
+  protected $contributionStatus;
+
   /**
    * Main IPN processing function.
    */
@@ -45,7 +55,7 @@ public function main() {
       $x_subscription_id = $this->getRecurProcessorID();
 
       if (!$this->isSuccess()) {
-        $errorMessage = ts('Subscription payment failed - %1', [1 => htmlspecialchars($input['response_reason_text'])]);
+        $errorMessage = ts('Subscription payment failed - %1', [1 => htmlspecialchars($this->getInput()['response_reason_text'])]);
         ContributionRecur::update(FALSE)
           ->addWhere('id', '=', $this->getContributionRecurID())
           ->setValues([
@@ -56,11 +66,30 @@ public function main() {
         \Civi::log('authorize_net')->info($errorMessage);
         return;
       }
-      if ($this->isSuccess() && ($this->getContributionStatus() !== 'Completed')) {
+      if ($this->getContributionStatus() !== 'Completed') {
         ContributionRecur::update(FALSE)->addWhere('id', '=', $this->getContributionRecurID())
           ->setValues(['trxn_id' => $this->getRecurProcessorID()])->execute();
+        $contributionID = $this->getContributionID();
+      }
+      else {
+        $contribution = civicrm_api3('Contribution', 'repeattransaction', [
+          'contribution_recur_id' => $this->getContributionRecurID(),
+          'receive_date' => $this->getInput()['receive_date'],
+          'payment_processor_id' => $this->getPaymentProcessorID(),
+          'trxn_id' => $this->getInput()['trxn_id'],
+          'amount' => $this->getAmount(),
+        ]);
+        $contributionID = $contribution['id'];
       }
-      $this->recur();
+      civicrm_api3('Payment', 'create', [
+        'trxn_id' => $this->getInput()['trxn_id'],
+        'trxn_date' => $this->getInput()['receive_date'],
+        'payment_processor_id' => $this->getPaymentProcessorID(),
+        'contribution_id' => $contributionID,
+        'total_amount' => $this->getAmount(),
+        'is_send_contribution_notification' => $this->getContributionRecur()->is_email_receipt,
+      ]);
+      $this->notify();
     }
     catch (CRM_Core_Exception $e) {
       Civi::log('authorize_net')->debug($e->getMessage());
@@ -71,13 +100,11 @@ public function main() {
   /**
    * @throws \CRM_Core_Exception
    */
-  public function recur() {
+  public function notify() {
     $recur = $this->getContributionRecur();
     $input = $this->getInput();
     $input['payment_processor_id'] = $this->getPaymentProcessorID();
 
-    $now = date('YmdHis');
-
     $isFirstOrLastRecurringPayment = FALSE;
     if ($this->isSuccess()) {
       // Approved
@@ -93,7 +120,6 @@ public function recur() {
       }
     }
 
-    CRM_Contribute_BAO_Contribution::completeOrder($input, $recur->id, $this->getContributionStatus() !== 'Completed' ? $this->getContributionID() : NULL);
     if ($isFirstOrLastRecurringPayment) {
       //send recurring Notification email for user
       CRM_Contribute_BAO_ContributionPage::recurringNotify($this->getContributionID(), TRUE,
@@ -117,7 +143,7 @@ public function getInput(): array {
     $input['response_reason_text'] = $this->retrieve('x_response_reason_text', 'String', FALSE);
     $input['subscription_paynum'] = $this->retrieve('x_subscription_paynum', 'Integer', FALSE, 0);
     $input['trxn_id'] = $this->retrieve('x_trans_id', 'String', FALSE);
-    $input['receive_date'] = $this->retrieve('receive_date', 'String', FALSE, date('YmdHis', strtotime('now')));
+    $input['receive_date'] = $this->retrieve('receive_date', 'String', FALSE, date('YmdHis', time()));
 
     if ($input['trxn_id']) {
       $input['is_test'] = 0;
@@ -126,9 +152,11 @@ public function getInput(): array {
     // Per CRM-17611 it would also not be passed back for a decline.
     elseif ($this->isSuccess()) {
       $input['is_test'] = 1;
-      $input['trxn_id'] = md5(uniqid(rand(), TRUE));
+      $input['trxn_id'] = $this->transactionID ?: md5(uniqid(mt_rand(), TRUE));
     }
+    $this->transactionID = $input['trxn_id'];
 
+    // None of this is used...
     $billingID = CRM_Core_BAO_LocationType::getBilling();
     $params = [
       'first_name' => 'x_first_name',
@@ -146,6 +174,17 @@ public function getInput(): array {
     return $input;
   }
 
+  /**
+   * Get amount.
+   *
+   * @return string
+   *
+   * @throws \CRM_Core_Exception
+   */
+  protected function getAmount(): string {
+    return $this->retrieve('x_amount', 'String');
+  }
+
   /**
    * Was the transaction successful.
    *
@@ -193,22 +232,26 @@ public function retrieve($name, $type, $abort = TRUE, $default = NULL) {
    */
   protected function getContributionRecurObject(string $processorID, int $contactID, int $contributionID) {
     // joining with contribution table for extra checks
-    $sql = "
+    $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()) {
+     WHERE cr.processor_id = %1 AND
+           (cr.contact_id = %2 OR co.id = %3)
+     LIMIT 1';
+    $contributionRecur = CRM_Core_DAO::executeQuery($sql, [
+      1 => [$processorID, 'String'],
+      2 => [$contactID, 'Integer'],
+      3 => [$contributionID, 'Integer'],
+    ]);
+    if (!$contributionRecur->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 => $contactID, 2 => $contRecur->contact_id]);
-      CRM_Core_Error::debug_log_message($message);
+    if ($contactID != $contributionRecur->contact_id) {
+      $message = ts('Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.', [1 => $contactID, 2 => $contributionRecur->contact_id]);
+      \Civi::log('authorize_net')->warning($message);
     }
-    return $contRecur;
+    return $contributionRecur;
   }
 
   /**
@@ -307,15 +350,18 @@ private function getContributionRecur(): CRM_Contribute_BAO_ContributionRecur {
    * @throws \CRM_Core_Exception
    */
   private function getContributionStatus(): string {
-    // Check if the contribution exists
-    // make sure contribution exists and is valid
-    $contribution = Contribution::get(FALSE)
-      ->addWhere('id', '=', $this->getContributionID())
-      ->addSelect('contribution_status_id:name')->execute()->first();
-    if (empty($contribution)) {
-      throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . $this->getContributionID(), NULL, ['context' => 'Could not find contribution record: ' . $this->getContributionID() . ' in IPN request: ' . print_r($this->getInput(), TRUE)]);
+    if (!$this->contributionStatus) {
+      // Check if the contribution exists
+      // make sure contribution exists and is valid
+      $contribution = Contribution::get(FALSE)
+        ->addWhere('id', '=', $this->getContributionID())
+        ->addSelect('contribution_status_id:name')->execute()->first();
+      if (empty($contribution)) {
+        throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . $this->getContributionID(), NULL, ['context' => 'Could not find contribution record: ' . $this->getContributionID() . ' in IPN request: ' . print_r($this->getInput(), TRUE)]);
+      }
+      $this->contributionStatus = $contribution['contribution_status_id:name'];
     }
-    return $contribution['contribution_status_id:name'];
+    return $this->contributionStatus;
   }
 
 }
diff --git a/CRM/Financial/BAO/Payment.php b/CRM/Financial/BAO/Payment.php
index 267a3082646f..97795f7cac19 100644
--- a/CRM/Financial/BAO/Payment.php
+++ b/CRM/Financial/BAO/Payment.php
@@ -188,6 +188,7 @@ public static function create(array $params): CRM_Financial_DAO_FinancialTrxn {
           'is_post_payment_create' => TRUE,
           'is_email_receipt' => $params['is_send_contribution_notification'],
           'trxn_date' => $params['trxn_date'],
+          'trxn_id' => $params['trxn_id'] ?? NULL,
           'payment_instrument_id' => $paymentTrxnParams['payment_instrument_id'],
           'payment_processor_id' => $paymentTrxnParams['payment_processor_id'] ?? '',
         ]);
diff --git a/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php b/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php
index 19376e92b45c..c2be5534b3b3 100644
--- a/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php
+++ b/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php
@@ -88,7 +88,7 @@ public function testIPNPaymentRecurNoReceipt(): void {
       'billing_middle_name' => '',
       'billing_last_name' => 'Adams',
       'billing_street_address-5' => time() . ' Lincoln St S',
-      'billing_city-5' => 'Maryknoll',
+      'billing_city-5' => 'Mary-knoll',
       'billing_state_province_id-5' => 1031,
       'billing_postal_code-5' => 10545,
       'billing_country_id-5' => 1228,