From d486b01e79c8a4e4533ccb53c17a7320c070e885 Mon Sep 17 00:00:00 2001 From: Pradeep Nayak Date: Tue, 17 Mar 2020 12:26:04 +0000 Subject: [PATCH] Fetch latest contribution id for building line items --- CRM/Contribute/BAO/ContributionRecur.php | 91 +++--- .../Contribute/BAO/ContributionRecurTest.php | 264 +++++++++++++++++- .../CRM/Core/Payment/AuthorizeNetIPNTest.php | 8 +- .../CRMTraits/Financial/OrderTrait.php | 4 +- 4 files changed, 325 insertions(+), 42 deletions(-) diff --git a/CRM/Contribute/BAO/ContributionRecur.php b/CRM/Contribute/BAO/ContributionRecur.php index b83716daa1..52a6bf6098 100644 --- a/CRM/Contribute/BAO/ContributionRecur.php +++ b/CRM/Contribute/BAO/ContributionRecur.php @@ -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; + } + } diff --git a/tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php b/tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php index ece30e5391..12d68ea855 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php @@ -14,8 +14,14 @@ * @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); + } + } diff --git a/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php b/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php index a83f966ac3..b402fbab73 100644 --- a/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php +++ b/tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php @@ -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", diff --git a/tests/phpunit/CRMTraits/Financial/OrderTrait.php b/tests/phpunit/CRMTraits/Financial/OrderTrait.php index 5911178b40..ea82bf58eb 100644 --- a/tests/phpunit/CRMTraits/Financial/OrderTrait.php +++ b/tests/phpunit/CRMTraits/Financial/OrderTrait.php @@ -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', -- 2.25.1