From 13a16f43d97c02105e603ed033284087c575b6ea Mon Sep 17 00:00:00 2001 From: eileen Date: Thu, 24 Nov 2016 15:03:39 +1300 Subject: [PATCH] CRM-19594 further fix on creating correct line items for memberships --- CRM/Contribute/Form/Contribution/Confirm.php | 3 +- CRM/Member/BAO/Membership.php | 31 +++++- CRM/Price/BAO/LineItem.php | 5 + api/v3/Membership.php | 1 + tests/phpunit/api/v3/ContributionPageTest.php | 102 ++++++++++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/CRM/Contribute/Form/Contribution/Confirm.php b/CRM/Contribute/Form/Contribution/Confirm.php index f04afe21e7..4242abfac1 100644 --- a/CRM/Contribute/Form/Contribution/Confirm.php +++ b/CRM/Contribute/Form/Contribution/Confirm.php @@ -1551,7 +1551,8 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr date('YmdHis'), CRM_Utils_Array::value('cms_contactID', $membershipParams), $customFieldsFormatted, $numTerms, $membershipID, $pending, - $contributionRecurID, $membershipSource, $isPayLater, $campaignId, array(), $membershipContribution + $contributionRecurID, $membershipSource, $isPayLater, $campaignId, array(), $membershipContribution, + $form->_lineItem ); $form->set('renewal_mode', $renewalMode); diff --git a/CRM/Member/BAO/Membership.php b/CRM/Member/BAO/Membership.php index 0b871f6782..52743165ce 100644 --- a/CRM/Member/BAO/Membership.php +++ b/CRM/Member/BAO/Membership.php @@ -227,7 +227,7 @@ class CRM_Member_BAO_Membership extends CRM_Member_DAO_Membership { * @param array $params * (reference ) an assoc array of name/value pairs. * @param array $ids - * The array that holds all the db ids. + * Deprecated parameter The array that holds all the db ids. * @param bool $skipRedirect * @param string $activityType * @@ -321,6 +321,7 @@ class CRM_Member_BAO_Membership extends CRM_Member_DAO_Membership { ); } + // This code ensures a line item is created but it is recomended you pass in 'skipLineItem' or 'line_item' if (empty($params['line_item']) && !empty($params['membership_type_id']) && empty($params['skipLineItem'])) { CRM_Price_BAO_LineItem::getLineItemArray($params, NULL, 'membership', $params['membership_type_id']); } @@ -341,7 +342,26 @@ class CRM_Member_BAO_Membership extends CRM_Member_DAO_Membership { CRM_Price_BAO_LineItem::deleteLineItems($ids['membership'], 'civicrm_membership'); } + // This could happen if there is no contribution or we are in one of many + // weird and wonderful flows. This is scary code. Keep adding tests. if (!empty($params['line_item']) && empty($ids['contribution'])) { + + foreach ($params['line_item'] as $priceSetId => $lineItems) { + foreach ($lineItems as $lineIndex => $lineItem) { + $lineMembershipType = CRM_Utils_Array::value('membership_type_id', $lineItem); + if (CRM_Utils_Array::value('contribution', $params)) { + $params['line_item'][$priceSetId][$lineIndex]['contribution_id'] = $params['contribution']->id; + } + if ($lineMembershipType && $lineMembershipType == CRM_Utils_Array::value('membership_type_id', $params)) { + $params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $membership->id; + $params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_membership'; + } + elseif (!$lineMembershipType && CRM_Utils_Array::value('contribution', $params)) { + $params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $params['contribution']->id; + $params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_contribution'; + } + } + } CRM_Price_BAO_LineItem::processPriceSet( $membership->id, $params['line_item'], @@ -1814,11 +1834,12 @@ INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND * @param $isPayLater * @param int $campaignId * @param array $formDates + * @param null|CRM_Contribute_BAO_Contribution $contribution * - * @throws CRM_Core_Exception * @return array + * @throws \CRM_Core_Exception */ - public static function processMembership($contactID, $membershipTypeID, $is_test, $changeToday, $modifiedID, $customFieldsFormatted, $numRenewTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $campaignId, $formDates = array(), $contribution = NULL) { + public static function processMembership($contactID, $membershipTypeID, $is_test, $changeToday, $modifiedID, $customFieldsFormatted, $numRenewTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $campaignId, $formDates = array(), $contribution = NULL, $lineItems = array()) { $renewalMode = $updateStatusId = FALSE; $allStatus = CRM_Member_PseudoConstant::membershipStatus(); $format = '%Y%m%d'; @@ -1850,6 +1871,7 @@ INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND 'status_id' => $currentMembership['status_id'], 'start_date' => $currentMembership['start_date'], 'end_date' => $currentMembership['end_date'], + 'line_item' => $lineItems, 'join_date' => $currentMembership['join_date'], 'membership_type_id' => $membershipTypeID, 'max_related' => !empty($membershipTypeDetails['max_related']) ? $membershipTypeDetails['max_related'] : NULL, @@ -2028,6 +2050,9 @@ INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND $memParams['contribution'] = $contribution; $memParams['custom'] = $customFieldsFormatted; + // Load all line items & process all in membership. Don't do in contribution. + // Relevant tests in api_v3_ContributionPageTest. + $memParams['line_item'] = $lineItems; $membership = self::create($memParams, $ids, FALSE, $activityType); // not sure why this statement is here, seems quite odd :( - Lobo: 12/26/2010 diff --git a/CRM/Price/BAO/LineItem.php b/CRM/Price/BAO/LineItem.php index 9b9b458306..75f55f6e93 100644 --- a/CRM/Price/BAO/LineItem.php +++ b/CRM/Price/BAO/LineItem.php @@ -69,6 +69,11 @@ class CRM_Price_BAO_LineItem extends CRM_Price_DAO_LineItem { if ($id) { unset($params['entity_id'], $params['entity_table']); } + else { + if (!isset($params['unit_price'])) { + $params['unit_price'] = 0; + } + } if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus() && CRM_Utils_Array::value('check_permissions', $params)) { if (empty($params['financial_type_id'])) { throw new Exception('Mandatory key(s) missing from params array: financial_type_id'); diff --git a/api/v3/Membership.php b/api/v3/Membership.php index cf056f9050..560dc81000 100644 --- a/api/v3/Membership.php +++ b/api/v3/Membership.php @@ -136,6 +136,7 @@ function civicrm_api3_membership_create($params) { $ids['userId'] = $params['contact_id']; } //for edit membership id should be present + // probably not required now. if (!empty($params['id'])) { $ids['membership'] = $params['id']; $action = CRM_Core_Action::UPDATE; diff --git a/tests/phpunit/api/v3/ContributionPageTest.php b/tests/phpunit/api/v3/ContributionPageTest.php index 2821ac7530..c877392f6f 100644 --- a/tests/phpunit/api/v3/ContributionPageTest.php +++ b/tests/phpunit/api/v3/ContributionPageTest.php @@ -655,6 +655,90 @@ 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->setUpMembershipContributionPage(); + $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] => reset($this->_ids['price_field_value']), + 'price_' . $this->_ids['price_field']['cont'] => 88, + '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' => $this->_ids['membership_type'], + 'payment_processor_id' => 1, + 'credit_card_number' => '4111111111111111', + 'credit_card_type' => 'Visa', + 'credit_card_exp_date' => array('M' => 9, 'Y' => 2040), + 'cvv2' => 123, + 'is_recur' => 1, + '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']); + $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', array()); + $this->assertEquals($membershipPayment['contribution_id'], $contribution['id']); + $membership = $this->callAPISuccessGetSingle('membership', array('id' => $membershipPayment['membership_id'])); + $this->assertEquals($membership['contact_id'], $contribution['contact_id']); + $this->assertEquals(1, $membership['status_id']); + $this->callAPISuccess('contribution_recur', 'getsingle', array('id' => $contribution['contribution_recur_id'])); + + $lines = $this->callAPISuccess('line_item', 'get', array('sequential' => 1, 'contribution_id' => $contribution['id'])); + $this->assertEquals(2, $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->callAPISuccessGetSingle('MembershipPayment', array('contribution_id' => $contribution['id'], 'membership_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(2, $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' => $membershipPayment['membership_id'])); + $this->assertEquals(date('Y-m-d', strtotime('+ 1 year', strtotime($membership['end_date']))), $renewedMembership['end_date']); + $recurringContribution = $this->callAPISuccess('contribution_recur', 'getsingle', array('id' => $contribution['contribution_recur_id'])); + $this->assertEquals($processor['payment_instrument_id'], $recurringContribution['payment_instrument_id']); + $this->assertEquals(5, $recurringContribution['contribution_status_id']); + } + /** * Test submit recurring membership with immediate confirmation (IATS style). * @@ -867,6 +951,24 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase { )); $this->_ids['price_field_value'][] = $priceFieldValue; } + $priceField = $this->callAPISuccess('price_field', 'create', array( + 'price_set_id' => reset($this->_ids['price_set']), + 'name' => 'Contribution', + 'label' => 'Contribution', + 'html_type' => 'Text', + 'sequential' => 1, + 'is_enter_qty' => 1, + )); + $this->_ids['price_field']['cont'] = $priceField['id']; + $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', array( + 'name' => 'contribution', + 'label' => 'Give me money', + 'amount' => 88, + 'financial_type_id' => 'Donation', + 'format.only_id' => TRUE, + 'price_field_id' => $priceField['id'], + )); + $this->_ids['price_field_value'][] = $priceFieldValue; } /** -- 2.25.1