CRM-19594 further fix on creating correct line items for memberships
authoreileen <emcnaughton@wikimedia.org>
Thu, 24 Nov 2016 02:03:39 +0000 (15:03 +1300)
committereileen <emcnaughton@wikimedia.org>
Thu, 24 Nov 2016 02:10:38 +0000 (15:10 +1300)
CRM/Contribute/Form/Contribution/Confirm.php
CRM/Member/BAO/Membership.php
CRM/Price/BAO/LineItem.php
api/v3/Membership.php
tests/phpunit/api/v3/ContributionPageTest.php

index f04afe21e722ff1de3bb001cb122653c9f65f4ea..4242abfac156fa0bf6e9913ac711b97078c30cb6 100644 (file)
@@ -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);
index 0b871f6782a44c5fe393397d4860ade7501f1cd1..52743165ce60ec363cab2e563b9c7fca41e5de4a 100644 (file)
@@ -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
index 9b9b458306c91e7c721090ed5f0839cc9e2a8484..75f55f6e93bc8486730c6559233d5ecd769b5962 100644 (file)
@@ -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');
index cf056f9050b9f444767eb4afb105c083d8c87f1b..560dc81000f6a60f47ecaabc828f8e957a12b32f 100644 (file)
@@ -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;
index 2821ac7530a80452c669548fd6f7dd21789b42a0..c877392f6f2f0fb7b4e18690537e0aaa4caa2f14 100644 (file)
@@ -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;
   }
 
   /**