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 [];
* @param \CRM_Contribute_BAO_Contribution $contribution
*
* @return array
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
*/
public static function addRecurLineItems($recurId, $contribution) {
$foundLineItems = FALSE;
* @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', [
'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);
}
/**
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;
+ }
+
}
* @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();
];
}
+ /**
+ * 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);
* 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);
/**
* 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);
/**
* Test we don't change unintended fields on API edit
+ *
+ * @throws \CRM_Core_Exception
*/
public function testUpdateRecur() {
$createParams = $this->_params;
/**
* 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);
/**
* 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;
/**
* 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);
$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);
+ }
+
}