From 65e172a32ec6b6d9d944202e9bda1e0f47c392e9 Mon Sep 17 00:00:00 2001 From: eileen Date: Fri, 25 Nov 2016 03:48:13 +1300 Subject: [PATCH] CRM-19594 fix line item memberships for price sets with multiple memberships --- CRM/Contribute/Form/Contribution/Confirm.php | 37 +++- CRM/Contribute/Form/Contribution/Main.php | 4 +- tests/phpunit/CiviTest/CiviUnitTestCase.php | 74 ++++++++ tests/phpunit/api/v3/ContributionPageTest.php | 159 +++++++++++++++++- tests/phpunit/api/v3/ContributionTest.php | 65 ------- 5 files changed, 267 insertions(+), 72 deletions(-) diff --git a/CRM/Contribute/Form/Contribution/Confirm.php b/CRM/Contribute/Form/Contribution/Confirm.php index 807a1e0cd4..e4fcfd4817 100644 --- a/CRM/Contribute/Form/Contribution/Confirm.php +++ b/CRM/Contribute/Form/Contribution/Confirm.php @@ -1456,6 +1456,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr $errors = $paymentResults = array(); $form->_values['isMembership'] = TRUE; $isRecurForFirstTransaction = CRM_Utils_Array::value('is_recur', $form->_values, CRM_Utils_Array::value('is_recur', $membershipParams)); + $unprocessedLineItems = $form->_lineItem; $totalAmount = $membershipParams['amount']; if ($isPaidMembership) { @@ -1521,7 +1522,26 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr //@todo it should no longer be possible for it to get to this point & membership to not be an array if (is_array($membershipTypeIDs) && !empty($membershipContributionID)) { $typesTerms = CRM_Utils_Array::value('types_terms', $membershipParams, array()); + + $membershipLines = $nonMembershipLines = array(); + foreach ($unprocessedLineItems as $priceSetID => $lines) { + foreach ($lines as $line) { + if (!empty($line['membership_type_id'])) { + $membershipLines[$line['membership_type_id']] = $line['price_field_value_id']; + } + } + } + + $i = 1; foreach ($membershipTypeIDs as $memType) { + if ($i < count($membershipTypeIDs)) { + $membershipLineItems[$priceSetID][$membershipLines[$memType]] = $unprocessedLineItems[$priceSetID][$membershipLines[$memType]]; + unset($unprocessedLineItems[$priceSetID][$membershipLines[$memType]]); + } + else { + $membershipLineItems = $unprocessedLineItems; + } + $i++; $numTerms = CRM_Utils_Array::value($memType, $typesTerms, 1); if (!empty($membershipContribution)) { $pendingStatus = CRM_Core_OptionGroup::getValue('contribution_status', 'Pending', 'name'); @@ -1557,7 +1577,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr $customFieldsFormatted, $numTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $campaignId, array(), $membershipContribution, - $form->_lineItem + $membershipLineItems ); $form->set('renewal_mode', $renewalMode); @@ -1936,9 +1956,20 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr else { $form->_params['payment_processor_id'] = 0; } + $priceFields = $priceFields[$priceSetID]['fields']; CRM_Price_BAO_PriceSet::processAmount($priceFields, $paramsProcessedForForm, $lineItems, 'civicrm_contribution'); $form->_lineItem = array($priceSetID => $lineItems); + $membershipPriceFieldIDs = array(); + foreach ((array) $lineItems as $lineItem) { + if (!empty($lineItem['membership_type_id'])) { + $form->set('useForMember', 1); + $form->_useForMember = 1; + $membershipPriceFieldIDs['id'] = $priceSetID; + $membershipPriceFieldIDs[] = $lineItem['price_field_value_id']; + } + } + $form->set('memberPriceFieldIDS', $membershipPriceFieldIDs); $form->processFormSubmission(CRM_Utils_Array::value('contact_id', $params)); } @@ -2325,7 +2356,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr $priceFieldIds = $this->get('memberPriceFieldIDS'); if (!empty($priceFieldIds)) { - $contributionTypeID = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $priceFieldIds['id'], 'financial_type_id'); + $financialTypeID = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $priceFieldIds['id'], 'financial_type_id'); unset($priceFieldIds['id']); $membershipTypeIds = array(); $membershipTypeTerms = array(); @@ -2344,7 +2375,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr } } $membershipParams['selectMembership'] = $membershipTypeIds; - $membershipParams['financial_type_id'] = $contributionTypeID; + $membershipParams['financial_type_id'] = $financialTypeID; $membershipParams['types_terms'] = $membershipTypeTerms; } if (!empty($membershipParams['selectMembership'])) { diff --git a/CRM/Contribute/Form/Contribution/Main.php b/CRM/Contribute/Form/Contribution/Main.php index 981fbfa105..363ca31890 100644 --- a/CRM/Contribute/Form/Contribution/Main.php +++ b/CRM/Contribute/Form/Contribution/Main.php @@ -1054,7 +1054,7 @@ class CRM_Contribute_Form_Contribution_Main extends CRM_Contribute_Form_Contribu $params['currencyID'] = CRM_Core_Config::singleton()->defaultCurrency; - $is_quick_config = 0; + // @todo refactor this & leverage it from the unit tests. if (!empty($params['priceSetId'])) { $is_quick_config = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $this->_priceSetId, 'is_quick_config'); if ($is_quick_config) { @@ -1118,7 +1118,7 @@ class CRM_Contribute_Form_Contribution_Main extends CRM_Contribute_Form_Contribu } } //If the membership & contribution is used in contribution page & not separate payment - $fieldId = $memPresent = $membershipLabel = $fieldOption = $is_quick_config = NULL; + $memPresent = $membershipLabel = $fieldOption = $is_quick_config = NULL; $proceFieldAmount = 0; if (property_exists($this, '_separateMembershipPayment') && $this->_separateMembershipPayment == 0) { $is_quick_config = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $this->_priceSetId, 'is_quick_config'); diff --git a/tests/phpunit/CiviTest/CiviUnitTestCase.php b/tests/phpunit/CiviTest/CiviUnitTestCase.php index 1fd3c05d1d..6e9e6c1541 100644 --- a/tests/phpunit/CiviTest/CiviUnitTestCase.php +++ b/tests/phpunit/CiviTest/CiviUnitTestCase.php @@ -3647,4 +3647,78 @@ AND ( TABLE_NAME LIKE 'civicrm_value_%' ) return Civi::settings()->set('contribution_invoice_settings', $contributeSetting); } + /** + * Create price set with contribution test for test setup. + * + * This could be merged with 4.5 function setup in api_v3_ContributionPageTest::setUpContributionPage + * on parent class at some point (fn is not in 4.4). + * + * @param $entity + * @param array $params + */ + public function createPriceSetWithPage($entity = NULL, $params = array()) { + $membershipTypeID = $this->membershipTypeCreate(array('name' => 'Special')); + $contributionPageResult = $this->callAPISuccess('contribution_page', 'create', array( + 'title' => "Test Contribution Page", + 'financial_type_id' => 1, + 'currency' => 'NZD', + 'goal_amount' => 50, + 'is_pay_later' => 1, + 'is_monetary' => TRUE, + 'is_email_receipt' => FALSE, + )); + $priceSet = $this->callAPISuccess('price_set', 'create', array( + 'is_quick_config' => 0, + 'extends' => 'CiviMember', + 'financial_type_id' => 1, + 'title' => 'my Page', + )); + $priceSetID = $priceSet['id']; + + CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID); + $priceField = $this->callAPISuccess('price_field', 'create', array( + 'price_set_id' => $priceSetID, + 'label' => 'Goat Breed', + 'html_type' => 'Radio', + )); + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'price_set_id' => $priceSetID, + 'price_field_id' => $priceField['id'], + 'label' => 'Long Haired Goat', + 'amount' => 20, + 'financial_type_id' => 'Donation', + 'membership_type_id' => $membershipTypeID, + 'membership_num_terms' => 1, + ) + ); + $this->_ids['price_field_value'] = array($priceFieldValue['id']); + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'price_set_id' => $priceSetID, + 'price_field_id' => $priceField['id'], + 'label' => 'Shoe-eating Goat', + 'amount' => 10, + 'financial_type_id' => 'Donation', + 'membership_type_id' => $membershipTypeID, + 'membership_num_terms' => 2, + ) + ); + $this->_ids['price_field_value'][] = $priceFieldValue['id']; + + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'price_set_id' => $priceSetID, + 'price_field_id' => $priceField['id'], + 'label' => 'Shoe-eating Goat', + 'amount' => 10, + 'financial_type_id' => 'Donation', + ) + ); + $this->_ids['price_field_value']['cont'] = $priceFieldValue['id']; + + $this->_ids['price_set'] = $priceSetID; + $this->_ids['contribution_page'] = $contributionPageResult['id']; + $this->_ids['price_field'] = array($priceField['id']); + + $this->_ids['membership_type'] = $membershipTypeID; + } + } diff --git a/tests/phpunit/api/v3/ContributionPageTest.php b/tests/phpunit/api/v3/ContributionPageTest.php index 014da9d8ff..fbe0e5c8bc 100644 --- a/tests/phpunit/api/v3/ContributionPageTest.php +++ b/tests/phpunit/api/v3/ContributionPageTest.php @@ -664,7 +664,7 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase { * - the first creates a new membership, completed contribution, in progress recurring. Check these * - create another - end date should be extended */ - public function testSubmitMembershipComplexPriceSetPaymentPaymentProcessorRecurInstantPayment() { + public function testSubmitMembershipComplexNonPriceSetPaymentPaymentProcessorRecurInstantPayment() { $this->params['is_recur'] = 1; $this->params['recur_frequency_unit'] = 'month'; // Add a membership so membership & contribution are not both 1. @@ -740,6 +740,140 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase { $this->assertEquals(5, $recurringContribution['contribution_status_id']); } + /** + * Test submit recurring membership with immediate confirmation (IATS style). + * + * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate + * processor (IATS style - denoted by returning trxn_id) + * - the first creates a new membership, completed contribution, in progress recurring. Check these + * - create another - end date should be extended + */ + public function testSubmitMembershipComplexPriceSetPaymentPaymentProcessorRecurInstantPayment() { + $this->params['is_recur'] = 1; + $this->params['recur_frequency_unit'] = 'month'; + // Add a membership so membership & contribution are not both 1. + $preExistingMembershipID = $this->contactMembershipCreate(array('contact_id' => $this->contactIds[0])); + $this->createPriceSetWithPage(); + $this->addSecondOrganizationMembershipToPriceSet(); + $this->setupPaymentProcessor(); + + $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor); + $dummyPP->setDoDirectPaymentResult(array('payment_status_id' => 1, 'trxn_id' => 'create_first_success')); + $processor = $dummyPP->getPaymentProcessor(); + + $submitParams = array( + 'price_' . $this->_ids['price_field'][0] => $this->_ids['price_field_value']['cont'], + 'price_' . $this->_ids['price_field']['org1'] => $this->_ids['price_field_value']['org1'], + 'price_' . $this->_ids['price_field']['org2'] => $this->_ids['price_field_value']['org2'], + 'id' => (int) $this->_ids['contribution_page'], + 'amount' => 10, + 'billing_first_name' => 'Billy', + 'billing_middle_name' => 'Goat', + 'billing_last_name' => 'Gruff', + 'email' => 'billy@goat.gruff', + 'selectMembership' => NULL, + 'payment_processor_id' => 1, + 'credit_card_number' => '4111111111111111', + 'credit_card_type' => 'Visa', + 'credit_card_exp_date' => array('M' => 9, 'Y' => 2040), + 'cvv2' => 123, + 'frequency_interval' => 1, + 'frequency_unit' => 'month', + ); + + $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL); + $contribution = $this->callAPISuccess('contribution', 'getsingle', array( + 'contribution_page_id' => $this->_ids['contribution_page'], + 'contribution_status_id' => 1, + )); + $this->assertEquals($processor['payment_instrument_id'], $contribution['payment_instrument_id']); + + $this->assertEquals('create_first_success', $contribution['trxn_id']); + $membershipPayments = $this->callAPISuccess('membership_payment', 'get', array( + 'sequential' => 1, + 'contribution_id' => $contribution['id'], + )); + $this->assertEquals(2, $membershipPayments['count']); + $lines = $this->callAPISuccess('line_item', 'get', array('sequential' => 1, 'contribution_id' => $contribution['id'])); + $this->assertEquals(3, $lines['count']); + $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']); + $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']); + $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']); + $this->assertEquals($contribution['id'], $lines['values'][1]['entity_id']); + $this->assertEquals('civicrm_membership', $lines['values'][2]['entity_table']); + $this->assertEquals($preExistingMembershipID + 2, $lines['values'][2]['entity_id']); + + $this->callAPISuccessGetSingle('MembershipPayment', array('contribution_id' => $contribution['id'], 'membership_id' => $preExistingMembershipID + 1)); + $membership = $this->callAPISuccessGetSingle('membership', array('id' => $preExistingMembershipID + 1)); + + //renew it with processor setting completed - should extend membership + $submitParams['contact_id'] = $contribution['contact_id']; + $dummyPP->setDoDirectPaymentResult(array('payment_status_id' => 1, 'trxn_id' => 'create_second_success')); + $this->callAPISuccess('contribution_page', 'submit', $submitParams); + $renewContribution = $this->callAPISuccess('contribution', 'getsingle', array( + 'id' => array('NOT IN' => array($contribution['id'])), + 'contribution_page_id' => $this->_ids['contribution_page'], + 'contribution_status_id' => 1, + )); + $lines = $this->callAPISuccess('line_item', 'get', array('sequential' => 1, 'contribution_id' => $renewContribution['id'])); + $this->assertEquals(3, $lines['count']); + $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']); + $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']); + $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']); + $this->assertEquals($renewContribution['id'], $lines['values'][1]['entity_id']); + + $renewedMembership = $this->callAPISuccessGetSingle('membership', array('id' => $preExistingMembershipID + 1)); + $this->assertEquals(date('Y-m-d', strtotime('+ 1 year', strtotime($membership['end_date']))), $renewedMembership['end_date']); + } + + /** + * Extend the price set with a second organisation's membership. + */ + public function addSecondOrganizationMembershipToPriceSet() { + $organization2ID = $this->organizationCreate(); + $membershipTypes = $this->callAPISuccess('MembershipType', 'get', array()); + $this->_ids['membership_type'] = array_keys($membershipTypes['values']); + $this->_ids['membership_type']['org2'] = $this->membershipTypeCreate(array('contact_id' => $organization2ID, 'name' => 'Org 2')); + $priceField = $this->callAPISuccess('PriceField', 'create', array( + 'price_set_id' => $this->_ids['price_set'], + 'html_type' => 'Radio', + 'name' => 'Org1 Price', + 'label' => 'Org1Price', + )); + $this->_ids['price_field']['org1'] = $priceField['id']; + + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'name' => 'org1 amount', + 'label' => 'org 1 Amount', + 'amount' => 2, + 'financial_type_id' => 'Member Dues', + 'format.only_id' => TRUE, + 'membership_type_id' => reset($this->_ids['membership_type']), + 'price_field_id' => $priceField['id'], + )); + $this->_ids['price_field_value']['org1'] = $priceFieldValue; + + $priceField = $this->callAPISuccess('PriceField', 'create', array( + 'price_set_id' => $this->_ids['price_set'], + 'html_type' => 'Radio', + 'name' => 'Org2 Price', + 'label' => 'Org2Price', + )); + $this->_ids['price_field']['org2'] = $priceField['id']; + + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'name' => 'org2 amount', + 'label' => 'org 2 Amount', + 'amount' => 200, + 'financial_type_id' => 'Member Dues', + 'format.only_id' => TRUE, + 'membership_type_id' => $this->_ids['membership_type']['org2'], + 'price_field_id' => $priceField['id'], + )); + $this->_ids['price_field_value']['org2'] = $priceFieldValue; + + } + /** * Test submit recurring membership with immediate confirmation (IATS style). * @@ -944,7 +1078,7 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase { $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( 'name' => 'membership_amount', 'label' => 'Membership Amount', - 'amount' => 1, + 'amount' => 2, 'financial_type_id' => 'Donation', 'format.only_id' => TRUE, 'membership_type_id' => $membershipTypeID, @@ -952,6 +1086,27 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase { )); $this->_ids['price_field_value'][] = $priceFieldValue; } + if (!empty($this->_ids['membership_type']['org2'])) { + $priceField = $this->callAPISuccess('price_field', 'create', array( + 'price_set_id' => reset($this->_ids['price_set']), + 'name' => 'membership_org2', + 'label' => 'Membership Org2', + 'html_type' => 'Checkbox', + 'sequential' => 1, + )); + $this->_ids['price_field']['org2'] = $priceField['id']; + + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'name' => 'membership_org2', + 'label' => 'Membership org 2', + 'amount' => 55, + 'financial_type_id' => 'Member Dues', + 'format.only_id' => TRUE, + 'membership_type_id' => $this->_ids['membership_type']['org2'], + 'price_field_id' => $priceField['id'], + )); + $this->_ids['price_field_value']['org2'] = $priceFieldValue; + } $priceField = $this->callAPISuccess('price_field', 'create', array( 'price_set_id' => reset($this->_ids['price_set']), 'name' => 'Contribution', diff --git a/tests/phpunit/api/v3/ContributionTest.php b/tests/phpunit/api/v3/ContributionTest.php index 573c7595a9..b757fc877a 100644 --- a/tests/phpunit/api/v3/ContributionTest.php +++ b/tests/phpunit/api/v3/ContributionTest.php @@ -1708,7 +1708,6 @@ class api_v3_ContributionTest extends CiviUnitTestCase { $this->revertTemplateToReservedTemplate(); } - /** * Test to check whether contact billing address is used when no contribution address */ @@ -2503,70 +2502,6 @@ class api_v3_ContributionTest extends CiviUnitTestCase { $this->contactDelete($this->_ids['contact']); } - - /** - * Create price set with contribution test for test setup. - * - * This could be merged with 4.5 function setup in api_v3_ContributionPageTest::setUpContributionPage - * on parent class at some point (fn is not in 4.4). - * - * @param $entity - * @param array $params - */ - public function createPriceSetWithPage($entity, $params = array()) { - $membershipTypeID = $this->membershipTypeCreate(); - $contributionPageResult = $this->callAPISuccess('contribution_page', 'create', array( - 'title' => "Test Contribution Page", - 'financial_type_id' => 1, - 'currency' => 'NZD', - 'goal_amount' => 50, - 'is_pay_later' => 1, - 'is_monetary' => TRUE, - 'is_email_receipt' => FALSE, - )); - $priceSet = $this->callAPISuccess('price_set', 'create', array( - 'is_quick_config' => 0, - 'extends' => 'CiviMember', - 'financial_type_id' => 1, - 'title' => 'my Page', - )); - $priceSetID = $priceSet['id']; - - CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID); - $priceField = $this->callAPISuccess('price_field', 'create', array( - 'price_set_id' => $priceSetID, - 'label' => 'Goat Breed', - 'html_type' => 'Radio', - )); - $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( - 'price_set_id' => $priceSetID, - 'price_field_id' => $priceField['id'], - 'label' => 'Long Haired Goat', - 'amount' => 20, - 'financial_type_id' => 'Donation', - 'membership_type_id' => $membershipTypeID, - 'membership_num_terms' => 1, - ) - ); - $this->_ids['price_field_value'] = array($priceFieldValue['id']); - $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( - 'price_set_id' => $priceSetID, - 'price_field_id' => $priceField['id'], - 'label' => 'Shoe-eating Goat', - 'amount' => 10, - 'financial_type_id' => 'Donation', - 'membership_type_id' => $membershipTypeID, - 'membership_num_terms' => 2, - ) - ); - $this->_ids['price_field_value'][] = $priceFieldValue['id']; - $this->_ids['price_set'] = $priceSetID; - $this->_ids['contribution_page'] = $contributionPageResult['id']; - $this->_ids['price_field'] = array($priceField['id']); - - $this->_ids['membership_type'] = $membershipTypeID; - } - /** * Set up a pending transaction with a specific price field id. * -- 2.25.1