diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index 131a9fa40046..0e7136b19fa4 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -4401,9 +4401,6 @@ public static function checkLineItems(&$params) { 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 9f5df990e5aa..0e5ff1817abf 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,106 @@ protected function setPriceSetIDFromSelectedField($fieldID): void { } } + /** + * 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 { + return str_replace('civicrm_', '', $this->lineItems[$index]['entity_table']); + } + + /** + * 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 528a25ef38c5..7ec2ae93b1b5 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 63edf813c8a6..7141f17f89b4 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php @@ -820,8 +820,6 @@ public function testcheckLineItems() { $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 e57891b89ea1..5ad26b09bedf 100644 --- a/tests/phpunit/api/v3/ContributionTest.php +++ b/tests/phpunit/api/v3/ContributionTest.php @@ -4345,24 +4345,31 @@ protected function setUpAutoRenewMembership($generalParams = [], $recurParams = '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', []);