Merge pull request #16442 from eileenmcnaughton/pree
authorMonish Deb <monish.deb@jmaconsulting.biz>
Mon, 10 Feb 2020 09:27:14 +0000 (14:57 +0530)
committerGitHub <noreply@github.com>
Mon, 10 Feb 2020 09:27:14 +0000 (14:57 +0530)
Fix backoffice participant partial payments to be stdised & not miscalculate net_amount

CRM/Contribute/BAO/Contribution.php
CRM/Event/Form/Participant.php
CRM/Financial/BAO/Payment.php
Civi/Test/ContactTestTrait.php
api/v3/Participant.php
tests/phpunit/CRM/Contribute/BAO/ContributionTest.php
tests/phpunit/CRM/Event/Form/ParticipantTest.php

index 4cc044b85c147073298edc14f649ae1b9f49c98b..00b4047fa9f26140710685f947647dbaa1eb0929 100644 (file)
@@ -4840,40 +4840,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac
     return '';
   }
 
-  /**
-   * Function to add payments for contribution for Partially Paid status
-   *
-   * @deprecated this is known to be flawed and possibly buggy.
-   *
-   * Replace with Order.create->Payment.create flow.
-   *
-   * @param array $contribution
-   *
-   * @throws \CiviCRM_API3_Exception
-   */
-  public static function addPayments($contribution) {
-    // get financial trxn which is a payment
-    $ftSql = "SELECT ft.id, ft.total_amount
-      FROM civicrm_financial_trxn ft
-      INNER JOIN civicrm_entity_financial_trxn eft ON eft.financial_trxn_id = ft.id AND eft.entity_table = 'civicrm_contribution'
-      WHERE eft.entity_id = %1 AND ft.is_payment = 1 ORDER BY ft.id DESC LIMIT 1";
-
-    $ftDao = CRM_Core_DAO::executeQuery($ftSql, [
-      1 => [
-        $contribution->id,
-        'Integer',
-      ],
-    ]);
-    $ftDao->fetch();
-
-    // store financial item Proportionaly.
-    $trxnParams = [
-      'total_amount' => $ftDao->total_amount,
-      'contribution_id' => $contribution->id,
-    ];
-    self::assignProportionalLineItems($trxnParams, $ftDao->id, $contribution->total_amount);
-  }
-
   /**
    * Function use to store line item proportionally in in entity financial trxn table
    *
index 1da70c725fe7d52c4c34082c53c254d718656ac3..37faf681f153517b80390b2fccad8adcc181f65a 100644 (file)
@@ -226,6 +226,31 @@ class CRM_Event_Form_Participant extends CRM_Contribute_Form_AbstractEditPayment
    */
   protected $participantRecord;
 
+  /**
+   * Params for creating a payment to add to the contribution.
+   *
+   * @var array
+   */
+  protected $createPaymentParams = [];
+
+  /**
+   * Get params to create payments.
+   *
+   * @return array
+   */
+  public function getCreatePaymentParams(): array {
+    return $this->createPaymentParams;
+  }
+
+  /**
+   * Set params to create payments.
+   *
+   * @param array $createPaymentParams
+   */
+  public function setCreatePaymentParams(array $createPaymentParams) {
+    $this->createPaymentParams = $createPaymentParams;
+  }
+
   /**
    * Explicitly declare the entity api name.
    */
@@ -1360,10 +1385,10 @@ class CRM_Event_Form_Participant extends CRM_Contribute_Form_AbstractEditPayment
           // CRM-13964 partial_payment_total
           if ($amountOwed > $params['total_amount']) {
             // the owed amount
-            $contributionParams['partial_payment_total'] = $amountOwed;
-            // the actual amount paid
-            $contributionParams['partial_amount_to_pay'] = $params['total_amount'];
-            $this->assign('balanceAmount', $contributionParams['partial_payment_total'] - $contributionParams['partial_amount_to_pay']);
+            $contributionParams['total_amount'] = $amountOwed;
+            $contributionParams['contribution_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
+            $this->assign('balanceAmount', $amountOwed - $params['total_amount']);
+            $this->storePaymentCreateParams($params);
           }
         }
 
@@ -1429,8 +1454,8 @@ class CRM_Event_Form_Participant extends CRM_Contribute_Form_AbstractEditPayment
         }
       }
       foreach ($contributions as $contribution) {
-        if ('Partially paid' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution->contribution_status_id)) {
-          CRM_Contribute_BAO_Contribution::addPayments($contribution);
+        if (!empty($this->getCreatePaymentParams())) {
+          civicrm_api3('Payment', 'create', array_merge(['contribution_id' => $contribution->id], $this->getCreatePaymentParams()));
         }
       }
     }
@@ -1467,7 +1492,7 @@ class CRM_Event_Form_Participant extends CRM_Contribute_Form_AbstractEditPayment
           }
         }
 
-        $this->assign('totalAmount', $contributionParams['total_amount']);
+        $this->assign('totalAmount', $params['total_amount'] ?? $contributionParams['total_amount']);
         $this->assign('isPrimary', 1);
         $this->assign('checkNumber', CRM_Utils_Array::value('check_number', $params));
       }
@@ -2235,4 +2260,26 @@ class CRM_Event_Form_Participant extends CRM_Contribute_Form_AbstractEditPayment
     return '';
   }
 
+  /**
+   * Store the parameters to create a payment, if approprite, on the form.
+   *
+   * @param array $params
+   *   Params as submitted.
+   */
+  protected function storePaymentCreateParams($params) {
+    if ('Completed' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution_status_id'])) {
+      $this->setCreatePaymentParams([
+        'total_amount' => $params['total_amount'],
+        'is_send_contribution_notification' => FALSE,
+        'payment_instrument_id' => $params['payment_instrument_id'],
+        'trxn_date' => $params['receive_date'] ?? date('Y-m-d'),
+        'trxn_id' => $params['trxn_id'],
+        'pan_truncation' => $params['pan_truncation'] ?? '',
+        'card_type_id' => $params['card_type_id'] ?? '',
+        'check_number' => $params['check_number'] ?? '',
+        'skipCleanMoney' => TRUE,
+      ]);
+    }
+  }
+
 }
index 122f13cb918ec4cbe2fecc230e74d92faa3fe18d..7cd09259cc78d89a4f583ed3358097559691252f 100644 (file)
@@ -45,6 +45,8 @@ class CRM_Financial_BAO_Payment {
     $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'];
     $paymentTrxnParams = array_intersect_key($params, array_fill_keys($whiteList, 1));
     $paymentTrxnParams['is_payment'] = 1;
+    // Really we should have a DB default.
+    $paymentTrxnParams['fee_amount'] = $paymentTrxnParams['fee_amount'] ?? 0;
 
     if (isset($paymentTrxnParams['payment_processor_id']) && empty($paymentTrxnParams['payment_processor_id'])) {
       // Don't pass 0 - ie the Pay Later processor as it is  a pseudo-processor.
@@ -148,6 +150,13 @@ class CRM_Financial_BAO_Payment {
     }
     elseif ($contributionStatus === 'Pending' && $params['total_amount'] > 0) {
       self::updateContributionStatus($contribution['id'], 'Partially Paid');
+      $participantPayments = civicrm_api3('ParticipantPayment', 'get', [
+        'contribution_id' => $contribution['id'],
+        'participant_id.status_id' => ['IN' => ['Pending from pay later', 'Pending from incomplete transaction']],
+      ])['values'];
+      foreach ($participantPayments as $participantPayment) {
+        civicrm_api3('Participant', 'create', ['id' => $participantPayment['participant_id'], 'status_id' => 'Partially paid']);
+      }
     }
     elseif ($contributionStatus === 'Completed' && ((float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($contribution['id'], TRUE) === 0.0)) {
       // If the contribution has previously been completed (fully paid) and now has total payments adding up to 0
index cb64e989e38c9897ea5126f12304fbf455d545ab..03b28597adf6632dbdd69cc67f5ba0a7b50a0bf9 100644 (file)
@@ -156,16 +156,17 @@ trait ContactTestTrait {
    *   For civicrm_contact_add api function call.
    *
    * @return int
-   *   id of Household created
-   * @throws \CRM_Core_Exception
+   *   id of contact created
    *
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
    */
   private function _contactCreate($params) {
     $result = civicrm_api3('contact', 'create', $params);
     if (!empty($result['is_error']) || empty($result['id'])) {
       throw new \CRM_Core_Exception('Could not create test contact, with message: ' . \CRM_Utils_Array::value('error_message', $result) . "\nBacktrace:" . \CRM_Utils_Array::value('trace', $result));
     }
-    return $result['id'];
+    return (int) $result['id'];
   }
 
   /**
index 29b56ae4ceea3d670f6c7801c398a0e550f96fc3..e70bc067cdcdfdc002b67e3106811ee4f3e83be9 100644 (file)
@@ -23,6 +23,9 @@
  *
  * @return array
  *   API result array
+ *
+ * @throws \CiviCRM_API3_Exception
+ * @throws \CRM_Core_Exception
  */
 function civicrm_api3_participant_create($params) {
   // Check that event id is not an template - should be done @ BAO layer.
index 5c6d66f1a3e74459e2a15bab2f7c336262937c28..52be41042974b257ece1b7c5b06cfc4a7b8caf54 100644 (file)
@@ -672,21 +672,6 @@ class CRM_Contribute_BAO_ContributionTest extends CiviUnitTestCase {
     $this->assertEquals(2, $financialTrxn->N, 'Mismatch count for is payment flag.');
   }
 
-  /**
-   * addPayments() method (add and edit modes of participant).
-   *
-   * Add Payments is part of an old, flawed, code flow.
-   */
-  public function testAddPayments() {
-    $contribution = $this->addParticipantWithContribution();
-    // Delete existing financial_trxns. This is because we are testing a code flow we
-    // want to deprecate & remove & the test relies on bad data asa starting point.
-    // End goal is the Order.create->Payment.create flow.
-    CRM_Core_DAO::executeQuery('DELETE FROM civicrm_entity_financial_trxn WHERE entity_table = "civicrm_financial_item"');
-    CRM_Contribute_BAO_Contribution::addPayments($contribution);
-    $this->checkItemValues($contribution);
-  }
-
   /**
    * checks db values for financial item
    */
index 399a8a927d76ab4a3dfb8c8a91d295dac266ed25..79f7ee481cd3b67859e3a08619de10d073786a93 100644 (file)
@@ -340,11 +340,11 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
       $event = $this->eventCreate($eventParams);
     }
 
-    $contactID = $this->individualCreate();
+    $this->ids['contact']['event'] = (int) $this->individualCreate();
     /** @var CRM_Event_Form_Participant $form */
     $form = $this->getFormObject('CRM_Event_Form_Participant');
     $form->_single = TRUE;
-    $form->_contactID = $form->_contactId = $contactID;
+    $form->_contactID = $form->_contactId = $this->ids['contact']['event'];
     $form->setCustomDataTypes();
     $form->_eventId = $event['id'];
     if (!empty($eventParams['is_monetary'])) {
@@ -547,7 +547,6 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
    * @param bool $isQuickConfig
    *
    * @throws \CRM_Core_Exception
-   * @throws \CiviCRM_API3_Exception
    */
   public function testSubmitPartialPayment($isQuickConfig) {
     $mut = new CiviMailUtils($this, TRUE);
@@ -573,7 +572,7 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
       'hidden_custom' => '1',
       'hidden_custom_group_count' => ['' => 1],
       'custom_4_-1' => '',
-      'contact_id' => $form->_contactID,
+      'contact_id' => $this->getContactID(),
       'event_id' => $this->getEventID(),
       'campaign_id' => '',
       'register_date' => '2020-01-31 00:50:00',
@@ -584,12 +583,80 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
       'MAX_FILE_SIZE' => '33554432',
     ];
     $form->submit($submitParams);
+    $this->assertPartialPaymentResult($isQuickConfig, $mut);
+  }
+
+  /**
+   * Test submitting a partially paid event registration, recording a pending contribution.
+   *
+   * This tests
+   *
+   * @dataProvider getBooleanDataProvider
+   *
+   * @param bool $isQuickConfig
+   *
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
+   */
+  public function testSubmitPendingPartiallyPaidAddPayment($isQuickConfig) {
+    $mut = new CiviMailUtils($this, TRUE);
+    $form = $this->getForm(['is_monetary' => 1]);
+    $this->callAPISuccess('PriceSet', 'create', ['is_quick_config' => $isQuickConfig, 'id' => $this->getPriceSetID()]);
+    $paymentInstrumentID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check');
+    $submitParams = $this->getRecordContributionParams('Partially paid', $form);
+    $form->submit($submitParams);
+    $this->callAPISuccess('Payment', 'create', [
+      'contribution_id' => $this->callAPISuccessGetValue('Contribution', ['return' => 'id']),
+      'total_amount'  => 20,
+      'check_number' => 879,
+      'payment_instrument_id' => $paymentInstrumentID,
+    ]);
+    $this->assertPartialPaymentResult($isQuickConfig, $mut);
+  }
+
+  /**
+   * Test submitting a partially paid event registration, recording a pending contribution.
+   *
+   * This tests
+   *
+   * @dataProvider getBooleanDataProvider
+   *
+   * @param bool $isQuickConfig
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function testSubmitPendingAddPayment($isQuickConfig) {
+    $mut = new CiviMailUtils($this, TRUE);
+    $form = $this->getForm(['is_monetary' => 1]);
+    $this->callAPISuccess('PriceSet', 'create', ['is_quick_config' => $isQuickConfig, 'id' => $this->getPriceSetID()]);
+    $paymentInstrumentID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check');
+    $submitParams = $this->getRecordContributionParams('Pending from pay later', 'Pending');
+    // Create the pending contribution for the full amount to be paid.
+    $submitParams['total_amount'] = 1550.55;
+    $form->submit($submitParams);
+    $this->callAPISuccess('Payment', 'create', [
+      'contribution_id' => $this->callAPISuccessGetValue('Contribution', ['return' => 'id']),
+      'total_amount'  => 20,
+      'check_number' => 879,
+      'payment_instrument_id' => $paymentInstrumentID,
+    ]);
+    $this->assertPartialPaymentResult($isQuickConfig, $mut, FALSE);
+  }
+
+  /**
+   * @param bool $isQuickConfig
+   * @param \CiviMailUtils $mut
+   * @param bool $isAmountPaidOnForm
+   *   Was the amount paid entered on the form (if so this should be on the receipt)
+   */
+  protected function assertPartialPaymentResult($isQuickConfig, CiviMailUtils $mut, $isAmountPaidOnForm = TRUE) {
+    $paymentInstrumentID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check');
     $contribution = $this->callAPISuccessGetSingle('Contribution', []);
     $expected = [
-      'contact_id' => $form->_contactID,
+      'contact_id' => $this->getContactID(),
       'total_amount' => '1550.55',
       'fee_amount' => '0.00',
-      'net_amount' => '20.00',
+      'net_amount' => '1550.55',
       'contribution_source' => 'I wrote this',
       'amount_level' => '',
       'is_template' => '0',
@@ -602,7 +669,7 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
 
     $participant = $this->callAPISuccessGetSingle('Participant', []);
     $this->assertAttributesEquals([
-      'contact_id' => $form->_contactID,
+      'contact_id' => $this->getContactID(),
       'event_title' => 'Annual CiviCRM meet',
       'participant_fee_level' => [0 => 'big - 1'],
       'participant_fee_amount' => '1550.55',
@@ -630,18 +697,6 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
       'tax_amount' => '0.00',
     ], $lineItem);
 
-    $financialTrxn = $this->callAPISuccessGetSingle('FinancialTrxn', ['is_payment' => 0]);
-    $this->assertAttributesEquals([
-      'to_financial_account_id' => '7',
-      'total_amount' => '1550.55',
-      'fee_amount' => '0.00',
-      'net_amount' => '1550.55',
-      'currency' => 'USD',
-      'status_id' => '1',
-      'payment_instrument_id' => $paymentInstrumentID,
-      'check_number' => '879',
-    ], $financialTrxn);
-
     $payment = $this->callAPISuccessGetSingle('FinancialTrxn', ['is_payment' => 1]);
     $this->assertAttributesEquals([
       'to_financial_account_id' => 6,
@@ -658,10 +713,10 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
     $financialItem = $this->callAPISuccessGetSingle('FinancialItem', []);
     $this->assertAttributesEquals([
       'description' => 'big',
-      'contact_id' => $form->_contactID,
+      'contact_id' => $this->getContactID(),
       'amount' => 1550.55,
       'currency' => 'USD',
-      'status_id' => 2,
+      'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Financial_BAO_FinancialItem', 'status_id', 'Unpaid'),
       'entity_table' => 'civicrm_line_item',
       'entity_id' => $lineItem['id'],
       'financial_account_id' => 4,
@@ -676,7 +731,7 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
       'Annual CiviCRM meet',
       'Registered Email',
       $isQuickConfig ? $this->formatMoneyInput(1550.55) . ' big - 1' : 'Price Field - big',
-      'Total Paid: $ 20.00',
+      $isAmountPaidOnForm ? 'Total Paid: $ 20.00' : ' ',
       'Balance: $ 1,530.55',
       'Financial Type: Event Fee',
       'Paid By: Check',
@@ -720,6 +775,47 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
     return (int) $this->_ids['price_field_value'][1];
   }
 
+  /**
+   * Get the parameters for recording a contribution.
+   *
+   * @param string $participantStatus
+   * @param string $contributionStatus
+   *
+   * @return array
+   */
+  protected function getRecordContributionParams($participantStatus, $contributionStatus): array {
+    $submitParams = [
+      'hidden_feeblock' => '1',
+      'hidden_eventFullMsg' => '',
+      'priceSetId' => $this->getPriceSetID(),
+      $this->getPriceFieldKey() => $this->getPriceFieldValueID(),
+      'check_number' => '879',
+      'record_contribution' => '1',
+      'financial_type_id' => '4',
+      'receive_date' => '2020-01-31 00:51:00',
+      'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check'),
+      'trxn_id' => '',
+      'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contributionStatus),
+      'total_amount' => '20',
+      'send_receipt' => '1',
+      'from_email_address' => $this->getFromEmailAddress(),
+      'receipt_text' => 'Contact the Development Department if you need to make any changes to your registration.',
+      'hidden_custom' => '1',
+      'hidden_custom_group_count' => ['' => 1],
+      'custom_4_-1' => '',
+      'contact_id' => $this->getContactID(),
+      'event_id' => $this->getEventID(),
+      'campaign_id' => '',
+      'register_date' => '2020-01-31 00:50:00',
+      'role_id' => [0 => CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'role_id', 'Attendee')],
+      'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'status_id', $participantStatus),
+      'source' => 'I wrote this',
+      'note' => 'I wrote a note',
+      'MAX_FILE_SIZE' => '33554432',
+    ];
+    return $submitParams;
+  }
+
   /**
    * Get the id of the created event.
    *
@@ -729,4 +825,13 @@ class CRM_Event_Form_ParticipantTest extends CiviUnitTestCase {
     return $this->ids['event']['event'];
   }
 
+  /**
+   * Get created contact ID.
+   *
+   * @return int
+   */
+  protected function getContactID(): int {
+    return $this->ids['contact']['event'];
+  }
+
 }