Fix error on recurring membership in the rc
authorEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 18 Dec 2023 11:01:40 +0000 (00:01 +1300)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 19 Dec 2023 04:13:53 +0000 (17:13 +1300)
Extend recur membership frequency test + fix to bi-monthly payments

CRM/Contribute/Form/Contribution/Confirm.php
CRM/Contribute/Form/ContributionBase.php
CRM/Financial/BAO/Order.php
templates/CRM/Contribute/Form/Contribution/ThankYou.tpl
tests/phpunit/CRM/Contribute/Form/Contribution/ConfirmTest.php
tests/phpunit/api/v3/ContributionPageTest.php

index 02db81ad495a73620c0c3f88bbdc8093b9a9a128..08c86427bd4238f9710d6c75f15140aef9bd7cfb 100644 (file)
@@ -1321,7 +1321,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr
   protected function processMembership($membershipParams, $contactID, $customFieldsFormatted, $premiumParams,
                                 $membershipLineItems): void {
 
-    $membershipTypeIDs = (array) $membershipParams['selectMembership'];
+    $membershipTypeIDs = array_keys($this->order->getMembershipTypes());
     $membershipTypes = CRM_Member_BAO_Membership::buildMembershipTypeValues($this, $membershipTypeIDs);
     $membershipType = empty($membershipTypes) ? [] : reset($membershipTypes);
 
index 306a6ab2af9b9d19a3f90be4e7739b2daa2c3a7f..d5d7561771df6763e3819cb0a22a22d474d59de2 100644 (file)
@@ -1143,30 +1143,31 @@ class CRM_Contribute_Form_ContributionBase extends CRM_Core_Form {
       $priceFieldValue = $this->_params["price_{$priceFieldId}"];
     }
     $selectedMembershipTypeID = $this->_values['fee'][$priceFieldId]['options'][$priceFieldValue]['membership_type_id'] ?? NULL;
-    if (!$selectedMembershipTypeID) {
+    if (!$selectedMembershipTypeID || !$this->getPaymentProcessorValue('is_recur')) {
       return;
     }
 
     // Check if membership the selected membership is automatically opted into auto renew or give user the option.
     // In the 2nd case we check that the user has in deed opted in (auto renew as at June 22 is the field name for the membership auto renew checkbox)
     // Also check that the payment Processor used can support recurring contributions.
-    $membershipTypes = CRM_Price_BAO_PriceSet::getMembershipTypesFromPriceSet($this->getPriceSetID());
-    if (in_array($selectedMembershipTypeID, $membershipTypes['autorenew_required'])
-      || (in_array($selectedMembershipTypeID, $membershipTypes['autorenew_optional']) &&
-        !empty($this->_params['auto_renew']))
-        && !empty($this->_paymentProcessor['is_recur'])
+    $membershipTypeDetails = CRM_Member_BAO_MembershipType::getMembershipType($selectedMembershipTypeID);
+    if (
+      // 2 means required
+      $membershipTypeDetails['auto_renew'] === 2
+      // 1 means optional - so they must also select the check box on the form
+      || ($membershipTypeDetails['auto_renew'] === 1 && !empty($this->_params['auto_renew']))
     ) {
       $this->_params['auto_renew'] = TRUE;
       $this->_params['is_recur'] = $this->_values['is_recur'] = 1;
-      $membershipTypeDetails = \Civi\Api4\MembershipType::get(FALSE)
-        ->addWhere('id', '=', $selectedMembershipTypeID)
-        ->execute()
-        ->first();
-      $this->_params['frequency_interval'] = $this->_params['frequency_interval'] ?? $this->_values['fee'][$priceFieldId]['options'][$priceFieldValue]['membership_num_terms'];
+      // If membership_num_terms is not specified on the the price field value (which seems not uncommon
+      // in default config) then the membership type provides the values.
+      // @todo - access the line item value from $this->getLineItems() rather than _values['fee']
+      $this->_params['frequency_interval'] = $this->getSubmittedValue('frequency_interval') ?? ($this->_values['fee'][$priceFieldId]['options'][$priceFieldValue]['membership_num_terms'] ?? ($membershipTypeDetails['duration_interval'] ?? 1));
       $this->_params['frequency_unit'] = $this->_params['frequency_unit'] ?? $membershipTypeDetails['duration_unit'];
     }
-    elseif (!$this->_separateMembershipPayment && (in_array($selectedMembershipTypeID, $membershipTypes['autorenew_required'])
-      || in_array($selectedMembershipTypeID, $membershipTypes['autorenew_optional']))) {
+    // This seems like it repeats the above with less care...
+    elseif (!$this->_separateMembershipPayment && ($membershipTypeDetails['auto_renew'] === 2
+      || $membershipTypeDetails['auto_renew'] === 1)) {
       // otherwise check if we have a separate membership payment setting as that will allow people to independently opt into recurring contributions and memberships
       // If we don't have that and the membership type is auto recur or opt into recur set is_recur to 0.
       $this->_params['is_recur'] = $this->_values['is_recur'] = 0;
index 8272744f13c1a22978e99ab562c549c799a5d22e..ec85b13fec0c9470fe371a95ecfd8d3ae296eccc 100644 (file)
@@ -957,6 +957,7 @@ class CRM_Financial_BAO_Order {
       $lineItem['membership_type_id'] = $lineItem['membership_type_id'] ?? NULL;
       if ($lineItem['membership_type_id']) {
         $lineItem['entity_table'] = 'civicrm_membership';
+        $lineItem['membership_num_terms'] = $lineItem['membership_num_terms'] ?:1;
       }
       $lineItem['title'] = $this->getLineItemTitle($lineItem);
       $lineItem['tax_rate'] = $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
index 948173ce9430edddcee63691259cf945ba0ab1ab..8e17cca2ad309ec09853d5bb7f5dcb21334d54df 100644 (file)
           {if !empty($auto_renew)} {* Auto-renew membership confirmation *}
             {crmRegion name="contribution-thankyou-recur-membership"}
               <br />
-              {if $installments > 1}
+              {if !$installments || $installments > 1}
                 {if $frequency_interval > 1}
                   <strong>{ts 1=$frequency_interval 2=$frequency_unit}This membership will be renewed automatically every %1 %2(s).{/ts}</strong>
                 {else}
index 21ea558e29884683417898eca4c9d0ffa588445f..8010ccded7939209824b2a9e6ddaaecad68b5b33 100644 (file)
@@ -11,6 +11,7 @@
 
 use Civi\Api4\Contribution;
 use Civi\Api4\LineItem;
+use Civi\Api4\PriceSet;
 use Civi\Api4\PriceSetEntity;
 use Civi\Test\ContributionPageTestTrait;
 use Civi\Test\FormTrait;
@@ -816,4 +817,109 @@ class CRM_Contribute_Form_Contribution_ConfirmTest extends CiviUnitTestCase {
     $this->assertEquals(5, $recurringContribution['contribution_status_id']);
   }
 
+  /**
+   * Helper function to set up contribution page which can be used to purchase a
+   * membership type for different intervals.
+   */
+  public function setUpMultiIntervalMembershipContributionPage(): void {
+    // These all have auto_renew set to 2 - ie require auto-renew.
+    $this->membershipTypeCreate([
+      'name' => 'monthly',
+      'auto_renew' => 2,
+      'duration_unit' => 'month',
+      'minimum_fee' => 10,
+    ], 'monthly');
+
+    $this->membershipTypeCreate([
+      'name' => 'bi_monthly',
+      'auto_renew' => 2,
+      'duration_unit' => 'month',
+      'duration_interval' => 2,
+      'minimum_fee' => 79,
+    ], 'bi_monthly');
+
+    $this->membershipTypeCreate([
+      'auto_renew' => 2,
+      'name' => 'yearly',
+      'duration_unit' => 'year',
+      'minimum_fee' => 100,
+    ], 'yearly');
+    $this->contributionPageQuickConfigCreate([], [], FALSE);
+  }
+
+  /**
+   * Test submit with a membership block in place.
+   *
+   * @dataProvider getBooleanDataProvider
+   *
+   * @param bool $isQuickConfig
+   */
+  public function testSubmitMultiIntervalMembershipContributionPage(bool $isQuickConfig): void {
+    $this->setUpMultiIntervalMembershipContributionPage();
+    PriceSet::update()->setValues(['is_quick_config' => $isQuickConfig])->addWhere('id', '=', $this->ids['PriceSet']['QuickConfig'])->execute();
+    if (!$isQuickConfig) {
+      $this->createTestEntity('PriceFieldValue', [
+        'name' => 'CRM-21177_12_Months',
+        'label' => 'CRM-21177 - 12 Months',
+        'amount' => 200,
+        'membership_num_terms' => 12,
+        'membership_type_id' => $this->ids['MembershipType']['monthly'],
+        'price_field_id' => $this->ids['PriceField']['membership_amount'],
+        'financial_type_id:name' => 'Member Dues',
+      ], 'membership_12_months');
+    }
+    $submitParams = [
+      'price_' . $this->ids['PriceField']['membership_amount'] => $this->ids['PriceFieldValue']['membership_monthly'],
+      'first_name' => 'Billy',
+      'last_name' => 'Gruff',
+      'email' => 'billy@goat.gruff',
+      'payment_processor_id' => $this->ids['PaymentProcessor']['dummy'],
+      'credit_card_number' => '4111111111111111',
+      'credit_card_type' => 'Visa',
+      'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
+      'cvv2' => 123,
+    ];
+    $this->submitOnlineContributionForm($submitParams,
+      $this->getContributionPageID());
+    $membership = $this->callAPISuccessGetSingle('Membership', []);
+    $contributionRecur = $this->callAPISuccessGetSingle('ContributionRecur', ['id' => $membership['contribution_recur_id']]);
+    $this->assertEquals('month', $contributionRecur['frequency_unit']);
+    $this->assertEquals(1, $contributionRecur['frequency_interval']);
+
+    $submitParams['price_' . $this->ids['PriceField']['membership_amount']] = $this->ids['PriceFieldValue']['membership_yearly'];
+    $this->submitOnlineContributionForm($submitParams,
+      $this->getContributionPageID());
+    $membership = $this->callAPISuccessGetSingle('Membership', ['membership_type_id' => $this->ids['MembershipType']['yearly']]);
+    $contributionRecur = $this->callAPISuccessGetSingle('ContributionRecur', ['id' => $membership['contribution_recur_id']]);
+    $this->assertEquals('year', $contributionRecur['frequency_unit']);
+    $this->assertEquals(1, $contributionRecur['frequency_interval']);
+
+    if (!$isQuickConfig) {
+      $submitParams['price_' . $this->ids['PriceField']['membership_amount']] = $this->ids['PriceFieldValue']['membership_12_months'];
+      $this->submitOnlineContributionForm($submitParams,
+        $this->getContributionPageID());
+      $contribution = $this->callAPISuccess('Contribution', 'get', [
+        'contribution_page_id' => $this->getContributionPageID(),
+        'sequential' => 1,
+        'api.ContributionRecur.getsingle' => [],
+        'version' => 3,
+      ]);
+      $this->assertEquals(1, $contribution['values'][0]['api.ContributionRecur.getsingle']['frequency_interval']);
+      $this->assertEquals(1, $contribution['values'][1]['api.ContributionRecur.getsingle']['frequency_interval']);
+      $this->assertEquals(12, $contribution['values'][2]['api.ContributionRecur.getsingle']['frequency_interval']);
+
+      $this->assertEquals('month', $contribution['values'][0]['api.ContributionRecur.getsingle']['frequency_unit']);
+      $this->assertEquals('year', $contribution['values'][1]['api.ContributionRecur.getsingle']['frequency_unit']);
+      $this->assertEquals('month', $contribution['values'][2]['api.ContributionRecur.getsingle']['frequency_unit']);
+    }
+
+    $submitParams['price_' . $this->ids['PriceField']['membership_amount']] = $this->ids['PriceFieldValue']['membership_bi_monthly'];
+    $this->submitOnlineContributionForm($submitParams,
+      $this->getContributionPageID());
+    $membership = $this->callAPISuccessGetSingle('Membership', ['membership_type_id' => $this->ids['MembershipType']['bi_monthly']]);
+    $contributionRecur = $this->callAPISuccessGetSingle('ContributionRecur', ['id' => $membership['contribution_recur_id']]);
+    $this->assertEquals('month', $contributionRecur['frequency_unit']);
+    $this->assertEquals(2, $contributionRecur['frequency_interval']);
+  }
+
 }
index e9f86f223469a4c9bf9cbf67db6e129bd10bd84b..495303f5acb075f653df3cdd9cc8a2329de46484 100644 (file)
@@ -1007,129 +1007,6 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase {
     }
   }
 
-  /**
-   * Helper function to set up contribution page which can be used to purchase a
-   * membership type for different intervals.
-   */
-  public function setUpMultiIntervalMembershipContributionPage(): void {
-    $this->setupPaymentProcessor();
-    $contributionPage = $this->callAPISuccess('ContributionPage', 'create', $this->params);
-    $this->ids['ContributionPage']['Membership'] = $contributionPage['id'];
-
-    $this->ids['MembershipTypeMonth'] = $this->membershipTypeCreate([
-      // force auto-renew
-      'auto_renew' => 2,
-      'duration_unit' => 'month',
-    ]);
-
-    $this->ids['MembershipTypeYear'] = $this->membershipTypeCreate([
-      // force auto-renew
-      'auto_renew' => 2,
-      'duration_unit' => 'year',
-    ]);
-
-    $priceSet = $this->callAPISuccess('PriceSet', 'create', [
-      'is_quick_config' => 0,
-      'extends' => 'CiviMember',
-      'financial_type_id' => 'Member Dues',
-      'title' => 'CRM-21177',
-    ]);
-    $this->_ids['price_set'] = $priceSet['id'];
-
-    $priceField = $this->callAPISuccess('price_field', 'create', [
-      'price_set_id' => $this->_ids['price_set'],
-      'name' => 'membership_type',
-      'label' => 'Membership Type',
-      'html_type' => 'Radio',
-    ]);
-    $this->_ids['price_field'] = $priceField['id'];
-
-    $priceFieldValueMonthly = $this->callAPISuccess('price_field_value', 'create', [
-      'name' => 'CRM-21177_Monthly',
-      'label' => 'CRM-21177 - Monthly',
-      'amount' => 20,
-      'membership_num_terms' => 1,
-      'membership_type_id' => $this->ids['MembershipTypeMonth'],
-      'price_field_id' => $this->_ids['price_field'],
-      'financial_type_id' => 'Member Dues',
-    ]);
-    $this->_ids['price_field_value_monthly'] = $priceFieldValueMonthly['id'];
-
-    $priceFieldValue12Months = $this->callAPISuccess('price_field_value', 'create', [
-      'name' => 'CRM-21177_12_Months',
-      'label' => 'CRM-21177 - 12 Months',
-      'amount' => 200,
-      'membership_num_terms' => 12,
-      'membership_type_id' => $this->ids['MembershipTypeMonth'],
-      'price_field_id' => $this->_ids['price_field'],
-      'financial_type_id' => 'Member Dues',
-    ]);
-    $this->_ids['price_field_value_12_months'] = $priceFieldValue12Months['id'];
-
-    $priceFieldValueYearly = $this->callAPISuccess('price_field_value', 'create', [
-      'name' => 'CRM-21177_Yearly',
-      'label' => 'CRM-21177 - Yearly',
-      'amount' => 200,
-      'membership_num_terms' => 1,
-      'membership_type_id' => $this->ids['MembershipTypeYear'],
-      'price_field_id' => $this->_ids['price_field'],
-      'financial_type_id' => 'Member Dues',
-    ]);
-    $this->_ids['price_field_value_yearly'] = $priceFieldValueYearly['id'];
-
-    CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $this->getContributionPageID(), $this->_ids['price_set']);
-
-    $this->callAPISuccess('membership_block', 'create', [
-      'entity_id' => $this->getContributionPageID(),
-      'entity_table' => 'civicrm_contribution_page',
-      'is_required' => TRUE,
-      'is_separate_payment' => FALSE,
-      'is_active' => TRUE,
-      'membership_type_default' => $this->ids['MembershipTypeMonth'],
-    ]);
-  }
-
-  /**
-   * Test submit with a membership block in place.
-   */
-  public function testSubmitMultiIntervalMembershipContributionPage(): void {
-    $this->setUpMultiIntervalMembershipContributionPage();
-    $submitParams = [
-      'price_' . $this->_ids['price_field'] => $this->_ids['price_field_value_monthly'],
-      'id' => $this->getContributionPageID(),
-      'amount' => 20,
-      'first_name' => 'Billy',
-      'last_name' => 'Gruff',
-      'email' => 'billy@goat.gruff',
-      'payment_processor_id' => $this->ids['PaymentProcessor']['dummy'],
-      'credit_card_number' => '4111111111111111',
-      'credit_card_type' => 'Visa',
-      'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
-      'cvv2' => 123,
-      'auto_renew' => 1,
-    ];
-    $this->callAPISuccess('contribution_page', 'submit', $submitParams);
-
-    $submitParams['price_' . $this->_ids['price_field']] = $this->_ids['price_field_value_yearly'];
-    $this->callAPISuccess('contribution_page', 'submit', $submitParams);
-
-    $submitParams['price_' . $this->_ids['price_field']] = $this->_ids['price_field_value_12_months'];
-    $this->callAPISuccess('contribution_page', 'submit', $submitParams);
-
-    $contribution = $this->callAPISuccess('Contribution', 'get', [
-      'contribution_page_id' => $this->getContributionPageID(),
-      'sequential' => 1,
-      'api.ContributionRecur.getsingle' => [],
-    ]);
-    $this->assertEquals(1, $contribution['values'][0]['api.ContributionRecur.getsingle']['frequency_interval']);
-    $this->assertEquals(1, $contribution['values'][1]['api.ContributionRecur.getsingle']['frequency_interval']);
-    $this->assertEquals(12, $contribution['values'][2]['api.ContributionRecur.getsingle']['frequency_interval']);
-
-    $this->assertEquals('month', $contribution['values'][0]['api.ContributionRecur.getsingle']['frequency_unit']);
-    $this->assertEquals('year', $contribution['values'][1]['api.ContributionRecur.getsingle']['frequency_unit']);
-    $this->assertEquals('month', $contribution['values'][2]['api.ContributionRecur.getsingle']['frequency_unit']);
-  }
-
   /**
    * Create a payment processor instance.
    */