Fix api Payment.create to support overpayments
authoreileen <emcnaughton@wikimedia.org>
Thu, 21 Nov 2019 10:12:00 +0000 (23:12 +1300)
committereileen <emcnaughton@wikimedia.org>
Thu, 21 Nov 2019 10:12:00 +0000 (23:12 +1300)
We've discussed this before - it's OK to add a payment to a fully paid contribution because ... life.

When this happens there should be no financial items linked to the over payment

CRM/Financial/BAO/Payment.php
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/api/v3/PaymentTest.php

index d43a9d54ea2a41410369299cf5d02579d6d993c1..7da4925b20f0b62b88c726b84781e9cf812dec78 100644 (file)
@@ -38,8 +38,8 @@ class CRM_Financial_BAO_Payment {
    */
   public static function create($params) {
     $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]);
-    $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($contribution['contribution_status_id'], 'name');
-    $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount']);
+    $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution['contribution_status_id']);
+    $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount'], $contributionStatus);
     $lineItems = self::getPayableLineItems($params);
 
     $whiteList = ['check_number', 'payment_processor_id', 'fee_amount', 'total_amount', 'contribution_id', 'net_amount', 'card_type_id', 'pan_truncation', 'trxn_result_code', 'payment_instrument_id', 'trxn_id', 'trxn_date'];
@@ -358,14 +358,18 @@ class CRM_Financial_BAO_Payment {
   }
 
   /**
-   * Does this payment complete the contribution
+   * Does this payment complete the contribution.
    *
    * @param int $contributionID
    * @param float $paymentAmount
+   * @param string $previousStatus
    *
    * @return bool
    */
-  protected static function isPaymentCompletesContribution($contributionID, $paymentAmount) {
+  protected static function isPaymentCompletesContribution($contributionID, $paymentAmount, $previousStatus) {
+    if ($previousStatus === 'Completed') {
+      return FALSE;
+    }
     $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionID);
     $cmp = bccomp($paymentAmount, $outstandingBalance, 5);
     return ($cmp == 0 || $cmp == 1);
index 273c243b184b7202d37cdc6d5c6546882b56a5b1..933fedfc23e1a966adb2d8526ac66572ad23af55 100644 (file)
@@ -3424,6 +3424,13 @@ AND    ( TABLE_NAME LIKE 'civicrm_value_%' )
    */
   protected function validatePayments($payments) {
     foreach ($payments as $payment) {
+      $balance = CRM_Contribute_BAO_Contribution::getContributionBalance($payment['contribution_id']);
+      if ($balance < 0 && $balance + $payment['total_amount'] === 0.0) {
+        // This is an overpayment situation. there are no financial items to allocate the overpayment.
+        // This is a pretty rough way at guessing which payment is the overpayment - but
+        // for the test suite it should be enough.
+        continue;
+      }
       $items = $this->callAPISuccess('EntityFinancialTrxn', 'get', [
         'financial_trxn_id' => $payment['id'],
         'entity_table' => 'civicrm_financial_item',
index 13ec8c9e8e9f93d1c30af56fb85dfdbb091598a9..9fd996d667981b35e8c8e60a68ed48dfb957a270 100644 (file)
@@ -687,6 +687,21 @@ class api_v3_PaymentTest extends CiviUnitTestCase {
     $this->validateAllPayments();
   }
 
+  /**
+   * Test that a contribution can be overpaid with the payment api.
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function testCreatePaymentOverPay() {
+    $contributionID = $this->contributionCreate(['contact_id' => $this->individualCreate()]);
+    $payment = $this->callAPISuccess('Payment', 'create', ['total_amount' => 5, 'order_id' => $contributionID]);
+    $contribution = $this->callAPISuccessGetSingle('Contribution', ['id' => $contributionID]);
+    $this->assertEquals('Completed', $contribution['contribution_status']);
+    $this->callAPISuccessGetCount('EntityFinancialTrxn', ['financial_trxn_id' => $payment['id'], 'entity_table' => 'civicrm_financial_item'], 0);
+    $this->validateAllPayments();
+    $this->validateAllContributions();
+  }
+
   /**
    * Test create payment api for paylater contribution
    *