Fetch latest contribution id for building line items
authorPradeep Nayak <pradpnayak@gmail.com>
Tue, 17 Mar 2020 12:26:04 +0000 (12:26 +0000)
committereileen <emcnaughton@wikimedia.org>
Sun, 31 May 2020 04:17:17 +0000 (16:17 +1200)
CRM/Contribute/BAO/ContributionRecur.php
tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php
tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php
tests/phpunit/CRMTraits/Financial/OrderTrait.php

index b83716daa12c0460f818d42a25c866e68cd63ec2..52a6bf609851df34ba4abf4b2c3e18fbf0e96863 100644 (file)
@@ -442,7 +442,8 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
     if ($templateContributions->count()) {
       $templateContribution = $templateContributions->first();
       $result = array_merge($templateContribution, $overrides);
-      $result['line_item'] = CRM_Contribute_BAO_ContributionRecur::calculateRecurLineItems($id, $result['total_amount'], $result['financial_type_id']);
+      $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($templateContribution['id']);
+      $result['line_item'] = self::reformatLineItemsForRepeatContribution($result['total_amount'], $result['financial_type_id'], $lineItems, (array) $templateContribution);
       return $result;
     }
     return [];
@@ -613,6 +614,8 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
    * @param \CRM_Contribute_BAO_Contribution $contribution
    *
    * @return array
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
    */
   public static function addRecurLineItems($recurId, $contribution) {
     $foundLineItems = FALSE;
@@ -901,6 +904,7 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
    * @param int $financial_type_id
    *
    * @return array
+   * @throws \CiviCRM_API3_Exception
    */
   public static function calculateRecurLineItems($recurId, $total_amount, $financial_type_id) {
     $originalContribution = civicrm_api3('Contribution', 'getsingle', [
@@ -910,41 +914,7 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
       'return' => ['id', 'financial_type_id'],
     ]);
     $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($originalContribution['id']);
-    $lineSets = [];
-    if (count($lineItems) == 1) {
-      foreach ($lineItems as $index => $lineItem) {
-        if ($lineItem['financial_type_id'] != $originalContribution['financial_type_id']) {
-          // CRM-20685, Repeattransaction produces incorrect Financial Type ID (in specific circumstance) - if number of lineItems = 1, So this conditional will set the financial_type_id as the original if line_item and contribution comes with different data.
-          $financial_type_id = $lineItem['financial_type_id'];
-        }
-        if ($financial_type_id) {
-          // CRM-17718 allow for possibility of changed financial type ID having been set prior to calling this.
-          $lineItem['financial_type_id'] = $financial_type_id;
-        }
-        $taxAmountMatches = FALSE;
-        if ((!empty($lineItem['tax_amount']) && ($lineItem['line_total'] + $lineItem['tax_amount']) == $total_amount)) {
-          $taxAmountMatches = TRUE;
-        }
-        if ($lineItem['line_total'] != $total_amount && !$taxAmountMatches) {
-          // We are dealing with a changed amount! Per CRM-16397 we can work out what to do with these
-          // if there is only one line item, and the UI should prevent this situation for those with more than one.
-          $lineItem['line_total'] = $total_amount;
-          $lineItem['unit_price'] = round($total_amount / $lineItem['qty'], 2);
-        }
-        $priceField = new CRM_Price_DAO_PriceField();
-        $priceField->id = $lineItem['price_field_id'];
-        $priceField->find(TRUE);
-        $lineSets[$priceField->price_set_id][$lineItem['price_field_id']] = $lineItem;
-      }
-    }
-    // CRM-19309 if more than one then just pass them through:
-    elseif (count($lineItems) > 1) {
-      foreach ($lineItems as $index => $lineItem) {
-        $lineSets[$index][$lineItem['price_field_id']] = $lineItem;
-      }
-    }
-
-    return $lineSets;
+    return self::reformatLineItemsForRepeatContribution($total_amount, $financial_type_id, $lineItems, $originalContribution);
   }
 
   /**
@@ -989,4 +959,53 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
     return CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
   }
 
+  /**
+   * Reformat line items for getTemplateContribution / repeat contribution.
+   *
+   * This is an extraction and may be subject to further cleanup.
+   *
+   * @param float $total_amount
+   * @param int $financial_type_id
+   * @param array $lineItems
+   * @param array $originalContribution
+   *
+   * @return array
+   */
+  protected static function reformatLineItemsForRepeatContribution($total_amount, $financial_type_id, array $lineItems, array $originalContribution): array {
+    $lineSets = [];
+    if (count($lineItems) == 1) {
+      foreach ($lineItems as $index => $lineItem) {
+        if ($lineItem['financial_type_id'] != $originalContribution['financial_type_id']) {
+          // CRM-20685, Repeattransaction produces incorrect Financial Type ID (in specific circumstance) - if number of lineItems = 1, So this conditional will set the financial_type_id as the original if line_item and contribution comes with different data.
+          $financial_type_id = $lineItem['financial_type_id'];
+        }
+        if ($financial_type_id) {
+          // CRM-17718 allow for possibility of changed financial type ID having been set prior to calling this.
+          $lineItem['financial_type_id'] = $financial_type_id;
+        }
+        $taxAmountMatches = FALSE;
+        if ((!empty($lineItem['tax_amount']) && ($lineItem['line_total'] + $lineItem['tax_amount']) == $total_amount)) {
+          $taxAmountMatches = TRUE;
+        }
+        if ($lineItem['line_total'] != $total_amount && !$taxAmountMatches) {
+          // We are dealing with a changed amount! Per CRM-16397 we can work out what to do with these
+          // if there is only one line item, and the UI should prevent this situation for those with more than one.
+          $lineItem['line_total'] = $total_amount;
+          $lineItem['unit_price'] = round($total_amount / $lineItem['qty'], 2);
+        }
+        $priceField = new CRM_Price_DAO_PriceField();
+        $priceField->id = $lineItem['price_field_id'];
+        $priceField->find(TRUE);
+        $lineSets[$priceField->price_set_id][$lineItem['price_field_id']] = $lineItem;
+      }
+    }
+    // CRM-19309 if more than one then just pass them through:
+    elseif (count($lineItems) > 1) {
+      foreach ($lineItems as $index => $lineItem) {
+        $lineSets[$index][$lineItem['price_field_id']] = $lineItem;
+      }
+    }
+    return $lineSets;
+  }
+
 }
index ece30e539106a2d6be36c0ab1b1cf0f47854fa9c..12d68ea8554a826a48a39d5e452fca6831059a46 100644 (file)
  * @group headless
  */
 class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
-  protected $_params = [];
 
+  use CRMTraits_Financial_OrderTrait;
+
+  /**
+   * Set up for test.
+   *
+   * @throws \CRM_Core_Exception
+   */
   public function setUp() {
     parent::setUp();
     $this->_ids['payment_processor'] = $this->paymentProcessorCreate();
@@ -49,14 +55,21 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
     ];
   }
 
+  /**
+   * Cleanup after test.
+   *
+   * @throws \CRM_Core_Exception
+   */
   public function teardown() {
-    $this->quickCleanup(['civicrm_contribution_recur', 'civicrm_payment_processor']);
+    $this->quickCleanUpFinancialEntities();
   }
 
   /**
    * Test that an object can be retrieved & saved (per CRM-14986).
    *
    * This has been causing a DB error so we are checking for absence of error
+   *
+   * @throws \CRM_Core_Exception
    */
   public function testFindSave() {
     $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
@@ -71,6 +84,8 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
    * Test cancellation works per CRM-14986.
    *
    * We are checking for absence of error.
+   *
+   * @throws \CRM_Core_Exception
    */
   public function testCancelRecur() {
     $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
@@ -80,6 +95,7 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
   /**
    * Test checking if contribution recur object can allow for changes to financial types.
    *
+   * @throws \CRM_Core_Exception
    */
   public function testSupportFinancialTypeChange() {
     $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
@@ -98,6 +114,8 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
 
   /**
    * Test we don't change unintended fields on API edit
+   *
+   * @throws \CRM_Core_Exception
    */
   public function testUpdateRecur() {
     $createParams = $this->_params;
@@ -117,6 +135,10 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
   /**
    * Check test contributions aren't picked up as template for non-test recurs
    *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
    */
   public function testGetTemplateContributionMatchTest1() {
     $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
@@ -150,6 +172,10 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
   /**
    * Check non-test contributions aren't picked up as template for test recurs
    *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
    */
   public function testGetTemplateContributionMatchTest() {
     $params = $this->_params;
@@ -187,6 +213,10 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
   /**
    * Test that is_template contribution is used where available
    *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
    */
   public function testGetTemplateContributionNewTemplate() {
     $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
@@ -218,4 +248,234 @@ class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
     $this->assertEquals($fetchedTemplate['id'], $templateContrib['id']);
   }
 
+  /**
+   * Test to check if correct membership is auto renewed.
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function testAutoRenewalWhenOneMemberIsDeceased() {
+    $contactId1 = $this->individualCreate();
+    $contactId2 = $this->individualCreate();
+    $membershipOrganizationId = $this->organizationCreate();
+
+    $this->createExtraneousContribution();
+    $this->callAPISuccess('Contribution', 'create', [
+      'contact_id' => $contactId1,
+      'receive_date' => '2010-01-20',
+      'financial_type_id' => 'Member Dues',
+      'contribution_status_id' => 'Completed',
+      'total_amount' => 150,
+    ]);
+
+    // create membership type
+    $membershipTypeId1 = $this->callAPISuccess('MembershipType', 'create', [
+      'domain_id' => 1,
+      'member_of_contact_id' => $membershipOrganizationId,
+      'financial_type_id' => 'Member Dues',
+      'duration_unit' => 'month',
+      'duration_interval' => 1,
+      'period_type' => 'rolling',
+      'minimum_fee' => 100,
+      'name' => 'Parent',
+    ])['id'];
+
+    $membershipTypeID = $this->callAPISuccess('MembershipType', 'create', [
+      'domain_id' => 1,
+      'member_of_contact_id' => $membershipOrganizationId,
+      'financial_type_id' => 'Member Dues',
+      'duration_unit' => 'month',
+      'duration_interval' => 1,
+      'period_type' => 'rolling',
+      'minimum_fee' => 50,
+      'name' => 'Child',
+    ])['id'];
+
+    $contactIDs = [
+      $contactId1 => $membershipTypeId1,
+      $contactId2 => $membershipTypeID,
+    ];
+
+    $contributionRecurId = $this->callAPISuccess('contribution_recur', 'create', $this->_params)['id'];
+
+    $priceFields = CRM_Price_BAO_PriceSet::getDefaultPriceSet('membership');
+
+    // prepare order api params.
+    $params = [
+      'contact_id' => $contactId1,
+      'receive_date' => '2010-01-20',
+      'financial_type_id' => 'Member Dues',
+      'contribution_status_id' => 'Pending',
+      'contribution_recur_id' => $contributionRecurId,
+      'total_amount' => 150,
+      'api.Payment.create' => ['total_amount' => 150],
+    ];
+
+    foreach ($priceFields as $priceField) {
+      $lineItems = [];
+      $contactId = array_search($priceField['membership_type_id'], $contactIDs);
+      $lineItems[1] = [
+        'price_field_id' => $priceField['priceFieldID'],
+        'price_field_value_id' => $priceField['priceFieldValueID'],
+        'label' => $priceField['label'],
+        'field_title' => $priceField['label'],
+        'qty' => 1,
+        'unit_price' => $priceField['amount'],
+        'line_total' => $priceField['amount'],
+        'financial_type_id' => $priceField['financial_type_id'],
+        'entity_table' => 'civicrm_membership',
+        'membership_type_id' => $priceField['membership_type_id'],
+      ];
+      $params['line_items'][] = [
+        'line_item' => $lineItems,
+        'params' => [
+          'contact_id' => $contactId,
+          'membership_type_id' => $priceField['membership_type_id'],
+          'source' => 'Payment',
+          'join_date' => '2020-04-28',
+          'start_date' => '2020-04-28',
+          'contribution_recur_id' => $contributionRecurId,
+          'status_id' => 'Pending',
+          'is_override' => 1,
+        ],
+      ];
+    }
+    $order = $this->callAPISuccess('Order', 'create', $params);
+    $contributionId = $order['id'];
+    $membershipId1 = $this->callAPISuccessGetValue('Membership', [
+      'contact_id' => $contactId1,
+      'membership_type_id' => $membershipTypeId1,
+      'return' => 'id',
+    ]);
+
+    $membershipId2 = $this->callAPISuccessGetValue('Membership', [
+      'contact_id' => $contactId2,
+      'membership_type_id' => $membershipTypeID,
+      'return' => 'id',
+    ]);
+
+    // First renewal (2nd payment).
+    $this->callAPISuccess('Contribution', 'repeattransaction', [
+      'original_contribution_id' => $contributionId,
+      'contribution_status_id' => 'Completed',
+    ]);
+
+    // Second Renewal (3rd payment).
+    $this->callAPISuccess('Contribution', 'repeattransaction', [
+      'original_contribution_id' => $contributionId,
+      'contribution_status_id' => 'Completed',
+    ]);
+
+    // Third renewal (4th payment).
+    $this->callAPISuccess('Contribution', 'repeattransaction', ['original_contribution_id' => $contributionId, 'contribution_status_id' => 'Completed']);
+
+    // check line item and membership payment count.
+    $this->validateAllCounts($membershipId1, 4);
+    $this->validateAllCounts($membershipId2, 4);
+
+    // check membership end date.
+    foreach ([$membershipId1, $membershipId2] as $mId) {
+      $endDate = $this->callAPISuccessGetValue('Membership', [
+        'id' => $mId,
+        'return' => 'end_date',
+      ]);
+      $this->assertEquals($endDate, '2020-08-27', ts('End date incorrect.'));
+    }
+
+    // At this moment Contact 2 is deceased, but we wait until payment is recorded in civi before marking the contact deceased.
+    // At payment Gateway we update the amount from 150 to 100
+    // IPN is recorded for subsequent payment (5th payment).
+    $contribution = $this->callAPISuccess('Contribution', 'repeattransaction', [
+      'original_contribution_id' => $contributionId,
+      'contribution_status_id' => 'Completed',
+      'total_amount' => '100',
+    ]);
+
+    // now we mark the contact2 as deceased.
+    $this->callAPISuccess('Contact', 'create', [
+      'id' => $contactId2,
+      'is_deceased' => 1,
+    ]);
+
+    // We delete latest membership payment and line item.
+    $lineItemId = $this->callAPISuccessGetValue('LineItem', [
+      'contribution_id' => $contribution['id'],
+      'entity_id' => $membershipId2,
+      'entity_table' => 'civicrm_membership',
+      'return' => 'id',
+    ]);
+
+    // No api to delete membership payment.
+    CRM_Core_DAO::executeQuery('
+      DELETE FROM civicrm_membership_payment
+      WHERE contribution_id = %1
+        AND membership_id = %2
+    ', [
+      1 => [$contribution['id'], 'Integer'],
+      2 => [$membershipId2, 'Integer'],
+    ]);
+
+    $this->callAPISuccess('LineItem', 'delete', [
+      'id' => $lineItemId,
+    ]);
+
+    // set membership recurring to null.
+    $this->callAPISuccess('Membership', 'create', [
+      'id' => $membershipId2,
+      'contribution_recur_id' => NULL,
+    ]);
+
+    // check line item and membership payment count.
+    $this->validateAllCounts($membershipId1, 5);
+    $this->validateAllCounts($membershipId2, 4);
+
+    $checkAgainst = $this->callAPISuccessGetSingle('Membership', [
+      'id' => $membershipId2,
+      'return' => ['end_date', 'status_id'],
+    ]);
+
+    // record next subsequent payment (6th payment).
+    $this->callAPISuccess('Contribution', 'repeattransaction', [
+      'original_contribution_id' => $contributionId,
+      'contribution_status_id' => 'Completed',
+      'total_amount' => '100',
+    ]);
+
+    // check membership id 1 is renewed
+    $endDate = $this->callAPISuccessGetValue('Membership', [
+      'id' => $membershipId1,
+      'return' => 'end_date',
+    ]);
+    $this->assertEquals($endDate, '2020-10-27', ts('End date incorrect.'));
+    // check line item and membership payment count.
+    $this->validateAllCounts($membershipId1, 6);
+    $this->validateAllCounts($membershipId2, 4);
+
+    // check if membership status and end date is not changed.
+    $membership2 = $this->callAPISuccessGetSingle('Membership', [
+      'id' => $membershipId2,
+      'return' => ['end_date', 'status_id'],
+    ]);
+    $this->assertSame($membership2, $checkAgainst);
+  }
+
+  /**
+   * Check line item and membership payment count.
+   *
+   * @param int $membershipId
+   * @param int $count
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function validateAllCounts($membershipId, $count) {
+    $memPayParams = [
+      'membership_id' => $membershipId,
+    ];
+    $lineItemParams = [
+      'entity_id' => $membershipId,
+      'entity_table' => 'civicrm_membership',
+    ];
+    $this->callAPISuccessGetCount('LineItem', $lineItemParams, $count);
+    $this->callAPISuccessGetCount('MembershipPayment', $memPayParams, $count);
+  }
+
 }
index a83f966ac39e0dfeca38ed39a15dece56fae97b7..b402fbab73c727a514c21d6943dff5cbc8a652ff 100644 (file)
@@ -180,6 +180,8 @@ class CRM_Core_Payment_AuthorizeNetIPNTest extends CiviUnitTestCase {
 
   /**
    * Test IPN response updates contribution_recur & contribution for first & second contribution
+   *
+   * @throws \CRM_Core_Exception
    */
   public function testIPNPaymentRecurSuccessSuppliedReceiveDate() {
     $this->setupRecurringPaymentProcessorTransaction();
@@ -242,6 +244,8 @@ class CRM_Core_Payment_AuthorizeNetIPNTest extends CiviUnitTestCase {
 
   /**
    * Test IPN response mails don't leak.
+   *
+   * @throws \CRM_Core_Exception
    */
   public function testIPNPaymentMembershipRecurSuccessNoLeakage() {
     $mut = new CiviMailUtils($this, TRUE);
@@ -377,9 +381,9 @@ class CRM_Core_Payment_AuthorizeNetIPNTest extends CiviUnitTestCase {
    */
   public function getRecurTransaction($params = []) {
     return array_merge([
-      "x_amount" => "200.00",
+      'x_amount' => '200.00',
       "x_country" => 'US',
-      "x_phone" => "",
+      'x_phone' => "",
       "x_fax" => "",
       "x_email" => "me@gmail.com",
       "x_description" => "lots of money",
index 5911178b4016073db27f3de3afc5d9e0da3f9e20..ea82bf58ebae43874d3b698f8e926f7af5313b4c 100644 (file)
@@ -28,7 +28,7 @@ trait CRMTraits_Financial_OrderTrait {
     $this->ids['contact'][0] = $this->individualCreate();
     $this->ids['membership_type'][0] = $this->membershipTypeCreate();
 
-    $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', array_merge([
+    $contributionRecur = $this->callAPISuccess('ContributionRecur', 'create', array_merge([
       'contact_id' => $this->_contactID,
       'amount' => 1000,
       'sequential' => 1,
@@ -97,7 +97,7 @@ trait CRMTraits_Financial_OrderTrait {
    */
   protected function createExtraneousContribution() {
     $this->contributionCreate([
-      'contact_id' => $this->_contactID,
+      'contact_id' => $this->individualCreate(),
       'is_test' => 1,
       'financial_type_id' => 1,
       'invoice_id' => 'abcd',