From ca44bb7efbabd1012e2566bd32964e70fda136cb Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Mon, 21 Jun 2021 12:36:24 +1200 Subject: [PATCH] Make Order api easier to use for default price set This changes the order api so that it is not necessary to figure out the details of the default price set when using it to create memberships. --- CRM/Contribute/BAO/Contribution.php | 7 +- CRM/Financial/BAO/Order.php | 126 ++++++++++++++++++ api/v3/Order.php | 43 +++--- .../CRM/Contribute/BAO/ContributionTest.php | 2 - tests/phpunit/api/v3/ContributionTest.php | 31 +++-- 5 files changed, 167 insertions(+), 42 deletions(-) diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index 131a9fa400..52bd39b095 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -194,7 +194,9 @@ class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution { $params['tax_amount'] = $taxAmount; $params['total_amount'] = $taxAmount + $lineTotal; } - if (isset($params['tax_amount']) && $params['tax_amount'] != $taxAmount && empty($params['skipLineItem'])) { + if (isset($params['tax_amount']) && empty($params['skipLineItem']) + && !CRM_Utils_Money::equals($params['tax_amount'], $taxAmount, ($params['currency'] ?? Civi::settings()->get('defaultCurrency'))) + ) { CRM_Core_Error::deprecatedWarning('passing in incorrect tax amounts is deprecated'); } @@ -4401,9 +4403,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac foreach ($params['line_items'] as &$lineItems) { foreach ($lineItems['line_item'] as &$item) { - if (empty($item['financial_type_id'])) { - $item['financial_type_id'] = $params['financial_type_id']; - } $lineItemAmount += $item['line_total'] + ($item['tax_amount'] ?? 0.00); } } diff --git a/CRM/Financial/BAO/Order.php b/CRM/Financial/BAO/Order.php index 9f5df990e5..00e0cc02c1 100644 --- a/CRM/Financial/BAO/Order.php +++ b/CRM/Financial/BAO/Order.php @@ -58,6 +58,29 @@ class CRM_Financial_BAO_Order { */ protected $overrideFinancialTypeID; + /** + * Financial type id to use for any lines where is is not provided. + * + * @var int + */ + protected $defaultFinancialTypeID; + + /** + * @return int + */ + public function getDefaultFinancialTypeID(): int { + return $this->defaultFinancialTypeID; + } + + /** + * Set the default financial type id to be used when the line has none. + * + * @param int|null $defaultFinancialTypeID + */ + public function setDefaultFinancialTypeID(?int $defaultFinancialTypeID): void { + $this->defaultFinancialTypeID = $defaultFinancialTypeID; + } + /** * Override for the total amount of the order. * @@ -634,4 +657,107 @@ class CRM_Financial_BAO_Order { } } + /** + * Set the line item. + * + * This function augments the line item where possible. The calling code + * should not attempt to set taxes. This function allows minimal values + * to be passed for the default price sets - ie if only membership_type_id is + * specified the price_field_id and price_value_id will be determined. + * + * @param array $lineItem + * @param int|string $index + * + * @throws \API_Exception + * @internal tested core code usage only. + * @internal use in tested core code only. + * + */ + public function setLineItem(array $lineItem, $index): void { + if (!empty($lineItem['price_field_id']) && !isset($this->priceSetID)) { + $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']); + } + if (!isset($lineItem['financial_type_id'])) { + $lineItem['financial_type_id'] = $this->getDefaultFinancialTypeID(); + } + if (!is_numeric($lineItem['financial_type_id'])) { + $lineItem['financial_type_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', $lineItem['financial_type_id']); + } + $lineItem['tax_amount'] = ($this->getTaxRate($lineItem['financial_type_id']) / 100) * $lineItem['line_total']; + if (!empty($lineItem['membership_type_id'])) { + $lineItem['entity_table'] = 'civicrm_membership'; + if (empty($lineItem['price_field_id']) && empty($lineItem['price_field_value_id'])) { + // If only the membership type is passed in we use the default price field. + if (!isset($this->priceSetID)) { + $this->setPriceSetToDefault('membership'); + } + $lineItem = $this->fillMembershipLine($lineItem); + } + } + $this->lineItems[$index] = $lineItem; + } + + /** + * Set a value on a line item. + * + * @internal only use in core tested code. + * + * @param string $name + * @param mixed $value + * @param string|int $index + */ + public function setLineItemValue(string $name, $value, $index): void { + $this->lineItems[$index][$name] = $value; + } + + /** + * @param int|string $index + * + * @return string + */ + public function getLineItemEntity($index):string { + // @todo - ensure entity_table is set in setLineItem, go back to enotices here. + return str_replace('civicrm_', '', ($this->lineItems[$index]['entity_table'] ?? 'contribution')); + } + + /** + * Get the ordered line item. + * + * @param string|int $index + * + * @return array + */ + public function getLineItem($index): array { + return $this->lineItems[$index]; + } + + /** + * Fills in additional data for the membership line. + * + * The minimum requirement is the membership_type_id and that priceSetID is set. + * + * @param array $lineItem + * + * @return array + */ + protected function fillMembershipLine(array $lineItem): array { + $fields = $this->getPriceFieldsMetadata(); + $field = reset($fields); + if (!isset($lineItem['price_field_value_id'])) { + foreach ($field['options'] as $option) { + if ((int) $option['membership_type_id'] === (int) $lineItem['membership_type_id']) { + $lineItem['price_field_id'] = $field['id']; + $lineItem['price_field_value_id'] = $option['id']; + $lineItem['qty'] = 1; + } + } + } + $option = $field['options'][$lineItem['price_field_value_id']]; + $lineItem['unit_price'] = $lineItem['line_total'] ?? $option['amount']; + $lineItem['label'] = $lineItem['label'] ?? $option['label']; + $lineItem['field_title'] = $lineItem['field_title'] ?? $option['label']; + $lineItem['financial_type_id'] = $lineItem['financial_type_id'] ?: ($this->getDefaultFinancialTypeID() ?? $option['financial_type_id']); + return $lineItem; + } + } diff --git a/api/v3/Order.php b/api/v3/Order.php index 528a25ef38..7ec2ae93b1 100644 --- a/api/v3/Order.php +++ b/api/v3/Order.php @@ -76,22 +76,20 @@ function civicrm_api3_order_create(array $params): array { $entity = NULL; $entityIds = []; $params['contribution_status_id'] = 'Pending'; - $priceSetID = NULL; + $order = new CRM_Financial_BAO_Order(); + $order->setDefaultFinancialTypeID($params['financial_type_id'] ?? NULL); if (!empty($params['line_items']) && is_array($params['line_items'])) { CRM_Contribute_BAO_Contribution::checkLineItems($params); - foreach ($params['line_items'] as $lineItems) { - $entityParams = $lineItems['params'] ?? []; - if (!empty($entityParams) && !empty($lineItems['line_item'])) { - $item = reset($lineItems['line_item']); - if (!empty($item['membership_type_id'])) { - $entity = 'membership'; - } - else { - $entity = str_replace('civicrm_', '', $item['entity_table']); - } + foreach ($params['line_items'] as $index => $lineItems) { + foreach ($lineItems['line_item'] as $innerIndex => $lineItem) { + $lineIndex = $index . '+' . $innerIndex; + $order->setLineItem($lineItem, $lineIndex); } + $entityParams = $lineItems['params'] ?? []; + $entity = $order->getLineItemEntity($lineIndex); + if ($entityParams) { $supportedEntity = TRUE; switch ($entity) { @@ -118,23 +116,20 @@ function civicrm_api3_order_create(array $params): array { $entityResult = civicrm_api3($entity, 'create', $entityParams); $params['contribution_mode'] = $entity; $entityIds[] = $params[$entity . '_id'] = $entityResult['id']; - foreach ($lineItems['line_item'] as &$items) { - $items['entity_id'] = $entityResult['id']; + foreach ($lineItems['line_item'] as $innerIndex => $lineItem) { + $lineIndex = $index . '+' . $innerIndex; + $order->setLineItemValue('entity_id', $entityResult['id'], $lineIndex); } } } - - if (empty($priceSetID)) { - $item = reset($lineItems['line_item']); - $priceSetID = (int) civicrm_api3('PriceField', 'getvalue', [ - 'return' => 'price_set_id', - 'id' => $item['price_field_id'], - ]); - $params['line_item'][$priceSetID] = []; - } - $params['line_item'][$priceSetID] = array_merge($params['line_item'][$priceSetID], $lineItems['line_item']); } + $priceSetID = $order->getPriceSetID(); + $params['line_item'][$priceSetID] = $order->getLineItems(); } + else { + $order->setPriceSetToDefault('contribution'); + } + $contributionParams = $params; // If this is nested we need to set sequential to 0 as sequential handling is done // in create_success & id will be miscalculated... @@ -149,7 +144,7 @@ function civicrm_api3_order_create(array $params): array { } $contribution = civicrm_api3('Contribution', 'create', $contributionParams); - $contribution['values'][$contribution['id']]['line_item'] = $params['line_item'][$priceSetID] ?? []; + $contribution['values'][$contribution['id']]['line_item'] = $order->getLineItems(); // add payments if ($entity && !empty($contribution['id'])) { diff --git a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php index 63edf813c8..7141f17f89 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php @@ -820,8 +820,6 @@ WHERE eft.entity_id = %1 AND ft.to_financial_account_id <> %2"; $e->getMessage() ); } - - $this->assertEquals(3, $params['line_items'][0]['line_item'][0]['financial_type_id']); $params['total_amount'] = 300; CRM_Contribute_BAO_Contribution::checkLineItems($params); diff --git a/tests/phpunit/api/v3/ContributionTest.php b/tests/phpunit/api/v3/ContributionTest.php index e57891b89e..5ad26b09be 100644 --- a/tests/phpunit/api/v3/ContributionTest.php +++ b/tests/phpunit/api/v3/ContributionTest.php @@ -4345,24 +4345,31 @@ class api_v3_ContributionTest extends CiviUnitTestCase { 'payment_processor_id' => $this->paymentProcessorID, ], $generalParams, $recurParams)); - $this->callAPISuccess('membership', 'create', [ - 'contact_id' => $newContact['id'], - 'contribution_recur_id' => $contributionRecur['id'], - 'financial_type_id' => 'Member Dues', - 'membership_type_id' => $membershipType['id'], - 'num_terms' => 1, - 'skipLineItem' => TRUE, - ]); - - CRM_Price_BAO_LineItem::getLineItemArray($this->_params, NULL, 'membership', $membershipType['id']); - $originalContribution = $this->callAPISuccess('contribution', 'create', array_merge( + $originalContribution = $this->callAPISuccess('Order', 'create', array_merge( $this->_params, [ 'contact_id' => $newContact['id'], 'contribution_recur_id' => $contributionRecur['id'], 'financial_type_id' => 'Member Dues', - 'contribution_status_id' => 1, + 'api.Payment.create' => ['total_amount' => 100, 'payment_instrument_id' => 'Credit card'], 'invoice_id' => 2345, + 'line_items' => [ + [ + 'line_item' => [ + [ + 'membership_type_id' => $membershipType['id'], + 'financial_type_id' => 'Member Dues', + 'line_total' => $generalParams['total_amount'] ?? 100, + ], + ], + 'params' => [ + 'contact_id' => $newContact['id'], + 'contribution_recur_id' => $contributionRecur['id'], + 'membership_type_id' => $membershipType['id'], + 'num_terms' => 1, + ], + ], + ], ], $generalParams) ); $lineItem = $this->callAPISuccess('LineItem', 'getsingle', []); -- 2.25.1