From b54a2bd1841ccc712b9c87fc89841c1b3cef66fb Mon Sep 17 00:00:00 2001
From: "Matthew Wire (MJW Consulting)" <mjw@mjwconsult.co.uk>
Date: Tue, 10 Sep 2019 18:33:35 +0100
Subject: [PATCH] Major refactor of MJWIPNTrait. Add function to set
 transaction ID for a payment

---
 CRM/Core/Payment/MJWIPNTrait.php | 277 ++++++++++++-------------------
 CRM/Core/Payment/MJWTrait.php    |  27 ++-
 2 files changed, 131 insertions(+), 173 deletions(-)

diff --git a/CRM/Core/Payment/MJWIPNTrait.php b/CRM/Core/Payment/MJWIPNTrait.php
index cea5434..7470174 100644
--- a/CRM/Core/Payment/MJWIPNTrait.php
+++ b/CRM/Core/Payment/MJWIPNTrait.php
@@ -8,15 +8,11 @@
  */
 
 trait CRM_Core_Payment_MJWIPNTrait {
-  /**********************
-   * MJW_Core_Payment_MJWIPNTrait: 20190901
-   * @requires MJW_Payment_Api: 20190901
-   *********************/
 
   /**
-   * @var array Payment processor
+   * @var \CRM_Core_Payment Payment processor
    */
-  private $_paymentProcessor;
+  protected $_paymentProcessor;
 
   /**
    * Do we send an email receipt for each contribution?
@@ -89,7 +85,7 @@ trait CRM_Core_Payment_MJWIPNTrait {
     }
 
     try {
-      $this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorId)->getPaymentProcessor();
+      $this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorId);
     }
     catch(Exception $e) {
       $this->exception('Failed to get payment processor');
@@ -97,235 +93,174 @@ trait CRM_Core_Payment_MJWIPNTrait {
   }
 
   /**
-   * @deprecated Use recordCancelled()
-   * Mark a contribution as cancelled and update related entities
-   *
-   * @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
+   * Record a refunded contribution
+   * @param array $params
    *
-   * @return bool
    * @throws \CiviCRM_API3_Exception
    */
-  protected function canceltransaction($params) {
-    return $this->incompletetransaction($params, 'cancel');
-  }
+  protected function updateContributionRefund($params) {
+    $this->checkRequiredParams('updateContributionRefund', ['id', 'total_amount'], $params);
 
-  /**
-   * @deprecated - Use recordFailed()
-   * Mark a contribution as failed and update related entities
-   *
-   * @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
-   *
-   * @return bool
-   * @throws \CiviCRM_API3_Exception
-   */
-  protected function failtransaction($params) {
-    return $this->incompletetransaction($params, 'fail');
+    if ($params['total_amount'] > 0) {
+      $params['total_amount'] = -$params['total_amount'];
+    }
+
+    if (empty($params['cancel_date'])) {
+      $params['cancel_date'] = date('YmdHis');
+    }
+
+    civicrm_api3('Contribution', 'create', $params);
   }
 
   /**
-   * @deprecated - Use recordXX methods
-   * Handler for failtransaction and canceltransaction - do not call directly
+   * Check that required params are present
    *
+   * @param string $description
+   *   For error logs
+   * @param array $requiredParams
+   *   Array of params that are required
    * @param array $params
-   * @param string $mode
-   *
-   * @return bool
-   * @throws \CiviCRM_API3_Exception
+   *   Array of params to check
    */
-  protected function incompletetransaction($params, $mode) {
-    $requiredParams = ['id', 'payment_processor_id'];
+  protected function checkRequiredParams($description, $requiredParams, $params) {
     foreach ($requiredParams as $required) {
       if (!isset($params[$required])) {
-        $this->exception('canceltransaction: Missing mandatory parameter: ' . $required);
+        $this->exception("{$description}: Missing mandatory parameter: {$required}");
       }
     }
-
-    if (isset($params['payment_processor_id'])) {
-      $input['payment_processor_id'] = $params['payment_processor_id'];
-    }
-    $contribution = new CRM_Contribute_BAO_Contribution();
-    $contribution->id = $params['id'];
-    if (!$contribution->find(TRUE)) {
-      throw new CiviCRM_API3_Exception('A valid contribution ID is required', 'invalid_data');
-    }
-
-    if (!$contribution->loadRelatedObjects($input, $ids, TRUE)) {
-      throw new CiviCRM_API3_Exception('failed to load related objects');
-    }
-
-    $input['trxn_id'] = !empty($params['trxn_id']) ? $params['trxn_id'] : $contribution->trxn_id;
-    if (!empty($params['fee_amount'])) {
-      $input['fee_amount'] = $params['fee_amount'];
-    }
-
-    $objects['contribution'] = &$contribution;
-    $objects = array_merge($objects, $contribution->_relatedObjects);
-
-    $transaction = new CRM_Core_Transaction();
-    switch ($mode) {
-      case 'cancel':
-        return $this->cancelled($objects, $transaction);
-
-      case 'fail':
-        return $this->failed($objects, $transaction);
-
-      default:
-        throw new CiviCRM_API3_Exception('Unknown incomplete transaction type: ' . $mode);
-    }
-  }
-
-  protected function recordPending($params) {
-    // Nothing to do
-    // @todo Maybe in the future record things like the pending reason if a payment is temporarily held?
   }
 
   /**
-   * Record a completed (successful) contribution
+   * Cancel a subscription (recurring contribution)
    * @param array $params
    *
    * @throws \CiviCRM_API3_Exception
    */
-  protected function recordCompleted($params) {
-    $description = 'recordCompleted';
-    $this->checkRequiredParams($description, ['contribution_id'], $params);
-    $contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
-
-    if (!empty($params['contribution_recur_id'])) {
-      $this->recordRecur($params, $description, $contributionStatusID);
-    }
-    else {
-      $params['id'] = $params['contribution_id'];
-      $pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
-      if (civicrm_api3('Contribution', 'getvalue', [
-          'return' => "contribution_status_id",
-          'id' => $params['id']
-        ]) == $pendingStatusId) {
-        civicrm_api3('Contribution', 'completetransaction', $params);
-      }
-    }
+  protected function updateRecurCancelled($params) {
+    $this->checkRequiredParams('updateRecurCancelled', ['id'], $params);
+    civicrm_api3('ContributionRecur', 'cancel', $params);
   }
 
   /**
-   * Record a failed contribution
+   * Update the subscription (recurring contribution) to a successful status
    * @param array $params
    *
    * @throws \CiviCRM_API3_Exception
    */
-  protected function recordFailed($params) {
-    $description = 'recordFailed';
-    $this->checkRequiredParams($description, ['contribution_id'], $params);
-    $contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Failed');
+  private function updateRecurSuccess($params) {
+    $this->checkRequiredParams('updateRecurSuccess', ['id'], $params);
+    $params['failure_count'] = 0;
+    $params['contribution_status_id'] = 'In Progress';
 
-    if (!empty($params['contribution_recur_id'])) {
-      $this->recordRecur($params, $description, $contributionStatusID);
-    }
-    else {
-      $this->recordSingle($params, $description, $contributionStatusID);
-    }
+    // Successful charge & more to come.
+    civicrm_api3('ContributionRecur', 'create', $params);
   }
 
   /**
-   * Record a cancelled contribution
+   * Update the subscription (recurring contribution) to a completed status
    * @param array $params
    *
    * @throws \CiviCRM_API3_Exception
    */
-  protected function recordCancelled($params) {
-    $description = 'recordCancelled';
-    $this->checkRequiredParams($description, ['contribution_id'], $params);
-    $contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Cancelled');
+  private function updateRecurCompleted($params) {
+    $this->checkRequiredParams('updateRecurCompleted', ['id'], $params);
+    $params['contribution_status_id'] = 'Completed';
 
-    if (!empty($params['contribution_recur_id'])) {
-      $this->recordRecur($params, $description, $contributionStatusID);
-    }
-    else {
-      $this->recordSingle($params, $description, $contributionStatusID);
-    }
+    civicrm_api3('ContributionRecur', 'create', $params);
   }
 
   /**
-   * Record a refunded contribution
+   * Update the subscription (recurring contribution) to a failing status
    * @param array $params
    *
    * @throws \CiviCRM_API3_Exception
    */
-  protected function recordRefund($params) {
-    $this->checkRequiredParams('recordRefund', ['contribution_id', 'total_amount'], $params);
+  private function updateRecurFailed($params) {
+    $this->checkRequiredParams('updateRecurFailed', ['id'], $params);
 
-    if ($params['total_amount'] > 0) {
-      $params['total_amount'] = -$params['total_amount'];
-    }
-    if (empty($params['trxn_date'])) {
-      $params['trxn_date'] = date('YmdHis');
-    }
+    $failureCount = civicrm_api3('ContributionRecur', 'getvalue', [
+      'id' => $params['id'],
+      'return' => 'failure_count',
+    ]);
+    $failureCount++;
+
+    $params['failure_count'] = $failureCount;
+    $params['contribution_status_id'] = 'Failed';
 
-    civicrm_api3('Payment', 'create', $params);
+    // Change the status of the Recurring and update failed attempts.
+    civicrm_api3('ContributionRecur', 'create', $params);
   }
 
   /**
-   * Check that required params are present
+   * Repeat a contribution (call the Contribution.repeattransaction API)
    *
-   * @param string $description
-   *   For error logs
-   * @param array $requiredParams
-   *   Array of params that are required
-   * @param array $params
-   *   Array of params to check
+   * @param string $status
+   *
+   * @throws \CiviCRM_API3_Exception
    */
-  protected function checkRequiredParams($description, $requiredParams, $params) {
-    foreach ($requiredParams as $required) {
-      if (!isset($params[$required])) {
-        $this->exception("{$description}: Missing mandatory parameter: {$required}");
-      }
+  private function repeatContribution($params) {
+    $params['is_email_receipt'] = $this->getSendEmailReceipt();
+    $params['trxn_id'] = $params['contribution_trxn_id'];
+
+    if ($params['previous_contribution']) {
+      $repeatParams['original_contribution_id'] = $params['previous_contribution']['id'];
     }
+    $contribution = civicrm_api3('Contribution', 'repeattransaction', $repeatParams);
+
+    $this->updatePaymentTrxnID($contribution['id'], $params['payment_trxn_id']);
   }
 
   /**
-   * Record a contribution against a recur (subscription)
-   * @param array $params
-   * @param string $description
-   * @param int $contributionStatusID
+   * Complete a pending contribution and update associated entities (recur/membership)
    *
    * @throws \CiviCRM_API3_Exception
    */
-  private function recordRecur($params, $description, $contributionStatusID) {
-    // Process as a payment in a recurring series
-    // We should have been passed a contribution_id, this either needs updating via completetransaction or repeating via repeattransaction
-    // If we've already processed it then we'll have a payment with the unique transaction ID
-    $this->checkRequiredParams($description, ['contribution_id', 'contribution_recur_id', 'payment_processor_transaction_id'], $params);
-    $matchingContributions = civicrm_api3('Mjwpayment', 'get_contribution', ['trxn_id' => $params['payment_processor_transaction_id']]);
-    if ($matchingContributions['count'] == 0) {
-      // This is a new transaction Id in a recurring series, trigger repeattransaction
-      // @fixme: We may need to consider handling partial payments on the same "invoice/order" (contribution)
-      //   but for now we assume that a new "completed" transaction means a new payment
-      $repeatParams = [
-        'contribution_status_id' => $contributionStatusID,
-        'original_contribution_id' => $params['contribution_id'],
-        'contribution_recur_id' => $params['contribution_recur_id'],
-        'trxn_id' => $params['payment_processor_transaction_id'],
-        'is_email_receipt' => $this->getSendEmailReceipt(),
-      ];
-      civicrm_api3('Contribution', 'repeattransaction', $repeatParams);
-    }
+  private function updateContributionCompleted($params) {
+    $this->checkRequiredParams('updateContributionCompleted', ['id', 'trxn_date', 'contribution_trxn_id', 'payment_trxn_id'], $params);
+    $params['payment_processor_id'] = $this->_paymentProcessor->getPaymentProcessor()['id'];
+    $params['is_email_receipt'] = $this->getSendEmailReceipt();
+    $params['trxn_id'] = $params['contribution_trxn_id'];
+
+    $contribution = civicrm_api3('Contribution', 'completetransaction', $params);
+    $this->updatePaymentTrxnID($contribution['id'], $params['payment_trxn_id']);
   }
 
   /**
-   * Record a change to a single contribution (eg. Failed/Cancelled).
-   *
-   * @param array $params
-   * @param string $description
-   * @param int $contributionStatusID
+   * Update a contribution to failed
+   * @param array $params ['id', 'receive_date'{, cancel_date, cancel_reason}]
    *
    * @throws \CiviCRM_API3_Exception
    */
-  private function recordSingle($params, $description, $contributionStatusID) {
-    $params['id'] = $params['contribution_id'];
-    $params['contribution_status_id'] = $contributionStatusID;
-    civicrm_api3('Contribution', 'create', $params);
+  private function updateContributionFailed($params) {
+    $this->checkRequiredParams('updateContributionFailed', ['id', 'receive_date', 'payment_trxn_id'], $params);
+    $contribution = civicrm_api3('Contribution', 'create', [
+      'id' => $params['id'],
+      'contribution_status_id' => 'Failed',
+      'receive_date' => $params['receive_date'],
+    ]);
+
+    $this->updatePaymentTrxnID($contribution['id'], $params['payment_trxn_id']);
   }
 
-  protected function recordSubscriptionCancelled($params = []) {
-    civicrm_api3('ContributionRecur', 'cancel', ['id' => $this->contribution_recur_id]);
+  /**
+   * Update the payment record so the trxn_id matches the actual transaction from the payment processor as we may have multiple transactions for a single payment (eg. failures, then success).
+   * @param int $contributionID
+   * @param string $trxnID
+   *
+   * @throws \CiviCRM_API3_Exception
+   */
+  private function updatePaymentTrxnID($contributionID, $trxnID) {
+    // @fixme: There needs to be a better way to do this!!
+    //   Contribution trxn_id = invoice_id, payment trxn_id = charge_id
+    //   but calling completetransaction does not allow us to do that.
+    $payment = civicrm_api3('Payment', 'get', [
+      'contribution_id' => $contributionID,
+      'options' => ['limit' => 1, 'sort' => "id DESC"],
+    ]);
+    civicrm_api3('FinancialTrxn', 'create', [
+      'id' => $payment['id'],
+      'trxn_id' => $trxnID,
+    ]);
   }
 
   /**
@@ -334,7 +269,7 @@ trait CRM_Core_Payment_MJWIPNTrait {
    * @param string $message
    */
   protected function exception($message) {
-    $errorMessage = $this->getPaymentProcessorLabel() . ' Exception: Event: ' . $this->event_type . ' Error: ' . $message;
+    $errorMessage = $this->_paymentProcessor->getPaymentProcessorLabel() . ' Exception: Event: ' . $this->event_type . ' Error: ' . $message;
     Civi::log()->debug($errorMessage);
     http_response_code(400);
     exit(1);
diff --git a/CRM/Core/Payment/MJWTrait.php b/CRM/Core/Payment/MJWTrait.php
index de1a7ea..085274a 100644
--- a/CRM/Core/Payment/MJWTrait.php
+++ b/CRM/Core/Payment/MJWTrait.php
@@ -17,6 +17,11 @@ trait CRM_Core_Payment_MJWTrait {
    */
   protected $_params = [];
 
+  /**
+   * @var string The unique charge/trxn reference from the payment processor
+   */
+  private $paymentProcessorTrxnID;
+
   /**
    * @var string The unique invoice/order reference from the payment processor
    */
@@ -391,10 +396,28 @@ trait CRM_Core_Payment_MJWTrait {
    *
    * @return string
    */
-  protected function getPaymentProcessorLabel() {
+  public function getPaymentProcessorLabel() {
     return $this->_paymentProcessor['name'];
   }
 
+  /**
+   * Set the payment processor Transaction ID
+   *
+   * @param string $trxnID
+   */
+  protected function setPaymentProcessorTrxnID($trxnID) {
+    $this->paymentProcessorTrxnID = $trxnID;
+  }
+
+  /**
+   * Get the payment processor Transaction ID
+   *
+   * @return string
+   */
+  protected function getPaymentProcessorTrxnID() {
+    return $this->paymentProcessorTrxnID;
+  }
+
   /**
    * Set the payment processor Order ID
    *
@@ -448,7 +471,7 @@ trait CRM_Core_Payment_MJWTrait {
    * @return array
    * @throws \CiviCRM_API3_Exception
    */
-  protected function endDoPayment($params, $contributionParams) {
+  protected function endDoPayment($params, $contributionParams = []) {
     $contributionParams['trxn_id'] = $this->getPaymentProcessorOrderID();
 
     if ($this->getContributionId($params)) {
-- 
GitLab