Skip to content

Commit

Permalink
Fixes getTemplateContribution to use a more reliable way to load line…
Browse files Browse the repository at this point in the history
… items

My efforts to add testing a 'deprecate weird stuff' have identified an odd and fragile
flow for the line items in getTemplateContribution. It calls
getLineItemsByContributionID which, as it turns out, substitues the
actual entity table with 'civicrm_contribution'.

Then this line of weird handling swoops in and saves the day.

https://github.com/civicrm/civicrm-core/pull/20775/files#diff-a16d4d7449cf5f3a0616d1d282a32f27ab6d3f7d2726d076c02ad1d4d655af41R393

This switches us to something cleaner than just loads the line items (with v4 LineItem.get) and
no weird handling
  • Loading branch information
eileenmcnaughton committed Jul 9, 2021
1 parent 09e790b commit 10ad7ea
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 99 deletions.
1 change: 0 additions & 1 deletion CRM/Contribute/BAO/Contribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -2497,7 +2497,6 @@ public static function contributionCount($contactId, $includeSoftCredit = TRUE)
* The count is out on how correct related entities wind up in this case.
*/
protected static function repeatTransaction(array $input, array $contributionParams) {

$templateContribution = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution(
(int) $contributionParams['contribution_recur_id'],
array_filter([
Expand Down
61 changes: 10 additions & 51 deletions CRM/Contribute/BAO/ContributionRecur.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,24 @@ public static function getTemplateContribution(int $id, $overrides = []): array
}
if ($templateContributions->count()) {
$templateContribution = $templateContributions->first();
$lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($templateContribution['id']);
$order = new CRM_Financial_BAO_Order();
$order->setTemplateContributionID($templateContribution['id']);
$order->setOverrideFinancialTypeID($overrides['financial_type_id'] ?? NULL);
$order->setOverrideTotalAmount($overrides['total_amount'] ?? NULL);
$order->setIsPermitOverrideFinancialTypeForMultipleLines(FALSE);
$lineItems = $order->getLineItems();
// We only permit the financial type to be overridden for single line items.
// Otherwise we need to figure out a whole lot of extra complexity.
// It's not UI-possible to alter financial_type_id for recurring contributions
// with more than one line item.
// The handling of the line items is managed in BAO_Order so this
// is whether we should override on the contribution. Arguably the 2 should
// be decoupled.
if (count($lineItems) > 1 && isset($overrides['financial_type_id'])) {
unset($overrides['financial_type_id']);
}
$result = array_merge($templateContribution, $overrides);
$result['line_item'] = self::reformatLineItemsForRepeatContribution($result['total_amount'], $result['financial_type_id'], $lineItems, (array) $templateContribution);
$result['line_item'][$order->getPriceSetID()] = $lineItems;
// If the template contribution was made on-behalf then add the
// relevant values to ensure the activity reflects that.
$relatedContact = CRM_Contribute_BAO_Contribution::getOnbehalfIds($result['id']);
Expand Down Expand Up @@ -993,53 +1001,4 @@ public static function buildOptions($fieldName, $context = NULL, $props = []) {
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;
}

}
172 changes: 146 additions & 26 deletions CRM/Financial/BAO/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
+--------------------------------------------------------------------+
*/

use Civi\Api4\LineItem;
use Civi\Api4\PriceField;
use Civi\Api4\PriceSet;

Expand Down Expand Up @@ -65,6 +66,75 @@ class CRM_Financial_BAO_Order {
*/
protected $defaultFinancialTypeID;

/**
* ID of a contribution to be used as a template.
*
* @var int
*/
protected $templateContributionID;

/**
* Should we permit the line item financial type to be overridden when there is more than one line.
*
* Historically the answer is 'yes' for v3 order api and 'no' for repeattransaction
* and backoffice forms.
*
* @var bool
*/
protected $isPermitOverrideFinancialTypeForMultipleLines = TRUE;

/**
* @return bool
*/
public function isPermitOverrideFinancialTypeForMultipleLines(): bool {
return $this->isPermitOverrideFinancialTypeForMultipleLines;
}

/**
* @param bool $isPermitOverrideFinancialTypeForMultipleLines
*/
public function setIsPermitOverrideFinancialTypeForMultipleLines(bool $isPermitOverrideFinancialTypeForMultipleLines): void {
$this->isPermitOverrideFinancialTypeForMultipleLines = $isPermitOverrideFinancialTypeForMultipleLines;
}

/**
* Number of line items.
*
* @var int
*/
protected $lineItemCount;

/**
* @return int
*/
public function getLineItemCount(): int {
if (!isset($this->lineItemCount)) {
$this->lineItemCount = count($this->getPriceOptions()) || count($this->lineItems);
}
return $this->lineItemCount;
}

/**
* @param int $lineItemCount
*/
public function setLineItemCount(int $lineItemCount): void {
$this->lineItemCount = $lineItemCount;
}

/**
* @return int|null
*/
public function getTemplateContributionID(): ?int {
return $this->templateContributionID;
}

/**
* @param int $templateContributionID
*/
public function setTemplateContributionID(int $templateContributionID): void {
$this->templateContributionID = $templateContributionID;
}

/**
* @return int
*/
Expand Down Expand Up @@ -179,7 +249,7 @@ public function _unserialize(array $data): void {
public function getOverrideTotalAmount() {
// The override amount is only valid for quick config price sets where more
// than one field has not been selected.
if (!$this->overrideTotalAmount || !$this->supportsOverrideAmount() || count($this->getPriceOptions()) > 1) {
if (!$this->overrideTotalAmount || $this->getLineItemCount() > 1) {
return FALSE;
}
return $this->overrideTotalAmount;
Expand All @@ -190,9 +260,9 @@ public function getOverrideTotalAmount() {
*
* @internal use in tested core code only.
*
* @param float $overrideTotalAmount
* @param float|null $overrideTotalAmount
*/
public function setOverrideTotalAmount(float $overrideTotalAmount): void {
public function setOverrideTotalAmount(?float $overrideTotalAmount): void {
$this->overrideTotalAmount = $overrideTotalAmount;
}

Expand All @@ -204,7 +274,11 @@ public function setOverrideTotalAmount(float $overrideTotalAmount): void {
* @return int| FALSE
*/
public function getOverrideFinancialTypeID() {
if (count($this->getPriceOptions()) !== 1) {
// We don't permit overrides if there is more than one line.
// The reason for this constraint may be more historical since
// the case could be made that if it is set it should be used and
// we have built out the tax calculations a lot now.
if (!$this->isPermitOverrideFinancialTypeForMultipleLines() && $this->getLineItemCount() > 1) {
return FALSE;
}
return $this->overrideFinancialTypeID ?? FALSE;
Expand All @@ -215,9 +289,9 @@ public function getOverrideFinancialTypeID() {
*
* @internal use in tested core code only.
*
* @param int $overrideFinancialTypeID
* @param int|null $overrideFinancialTypeID
*/
public function setOverrideFinancialTypeID(int $overrideFinancialTypeID): void {
public function setOverrideFinancialTypeID(?int $overrideFinancialTypeID): void {
$this->overrideFinancialTypeID = $overrideFinancialTypeID;
}

Expand Down Expand Up @@ -265,25 +339,13 @@ public function setPriceSetToDefault(string $component): void {
->first()['id'];
}

/**
* Is overriding the total amount valid for this price set.
*
* @internal tested. core code use only.
*
* @return bool
*/
public function supportsOverrideAmount(): bool {
return (bool) $this->getPriceSetMetadata()['is_quick_config'];
}

/**
* Set price set ID based on the contribution page id.
*
* @internal use in tested core code only.
*
* @param int $contributionPageID
*
* @throws \CiviCRM_API3_Exception
*/
public function setPriceSetIDByContributionPageID(int $contributionPageID): void {
$this->setPriceSetIDByEntity('contribution_page', $contributionPageID);
Expand Down Expand Up @@ -550,7 +612,8 @@ public function getRenewableMembershipTypes(): array {

/**
* @return array
* @throws \CiviCRM_API3_Exception
*
* @throws \API_Exception
*/
protected function calculateLineItems(): array {
$lineItems = [];
Expand All @@ -564,17 +627,30 @@ protected function calculateLineItems(): array {

// Dummy value to prevent e-notice in getLine. We calculate tax in this class.
$params['financial_type_id'] = 0;
foreach ($this->getPriceOptions() as $fieldID => $valueID) {
$this->setPriceSetIDFromSelectedField($fieldID);
$throwAwayArray = [];
// @todo - still using getLine for now but better to bring it to this class & do a better job.
$newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
foreach ($newLines as $newLine) {
$lineItems[$newLine['price_field_value_id']] = $newLine;
if ($this->getTemplateContributionID()) {
$lineItems = $this->getLinesFromTemplateContribution();
}
else {
foreach ($this->getPriceOptions() as $fieldID => $valueID) {
$this->setPriceSetIDFromSelectedField($fieldID);
$throwAwayArray = [];
// @todo - still using getLine for now but better to bring it to this class & do a better job.
$newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
foreach ($newLines as $newLine) {
$lineItems[$newLine['price_field_value_id']] = $newLine;
}
}
}
// Set the line item count here because it is needed to determine whether
// we can use overrides and would not be set yet if we have loaded them from
// a template contribution.
$this->setLineItemCount(count($lineItems));

foreach ($lineItems as &$lineItem) {
// Set the price set id if not set above. Note that the above
// requires it for line retrieval but we want to fix that as it
// should not be required at that point.
$this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
// Set any pre-calculation to zero as we will calculate.
$lineItem['tax_amount'] = 0;
if ($this->getOverrideFinancialTypeID() !== FALSE) {
Expand Down Expand Up @@ -801,4 +877,48 @@ protected function addTotalsToLineBasedOnOverrideTotal(int $financialTypeID, arr
}
}

/**
* Get the line items from a template.
*
* @return \Civi\Api4\Generic\Result
*
* @throws \API_Exception
*/
protected function getLinesFromTemplateContribution(): array {
$lines = $this->getLinesForContribution();
foreach ($lines as &$line) {
// The apiv4 insists on adding id - so let it get all the details
// and we will filter out those that are not part of a template here.
unset($line['id'], $line['contribution_id']);
}
return $lines;
}

/**
* @return array
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function getLinesForContribution(): array {
return (array) LineItem::get(FALSE)
->addWhere('contribution_id', '=', $this->getTemplateContributionID())
->setSelect([
'contribution_id',
'entity_id',
'entity_table',
'price_field_id',
'price_field_value_id',
'financial_type_id',
'label',
'qty',
'unit_price',
'line_total',
'tax_amount',
'non_deductible_amount',
'participant_count',
'membership_num_terms',
])
->execute();
}

}
1 change: 0 additions & 1 deletion tests/phpunit/CRM/Contribute/BAO/ContributionRecurTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,6 @@ public function testGetTemplateContributionMatchTest(): void {
* 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
*/
Expand Down
4 changes: 2 additions & 2 deletions tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public function testIPNPaymentRecurSuccess(): void {
$this->assertEquals(1, $contribution1['contribution_status_id']);
$this->assertEquals('8XA571746W2698126', $contribution1['trxn_id']);
// source gets set by processor
$this->assertTrue(substr($contribution1['contribution_source'], 0, 20) === 'Online Contribution:');
$this->assertEquals('Online Contribution:', substr($contribution1['contribution_source'], 0, 20));
$contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID]);
$this->assertEquals(5, $contributionRecur['contribution_status_id']);
$paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurSubsequentTransaction());
Expand Down Expand Up @@ -207,7 +207,7 @@ public function testIPNPaymentMembershipRecurSuccess() {
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public function testIPNPaymentInputMembershipRecurSuccess() {
public function testIPNPaymentInputMembershipRecurSuccess(): void {
$durationUnit = 'year';
$this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
$membershipPayment = $this->callAPISuccessGetSingle('membership_payment', []);
Expand Down
Loading

0 comments on commit 10ad7ea

Please sign in to comment.