3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
12 use Civi\Api4\LineItem
;
13 use Civi\Api4\PriceField
;
14 use Civi\Api4\PriceFieldValue
;
15 use Civi\Api4\PriceSet
;
20 * @copyright CiviCRM LLC https://civicrm.org/licensing
24 * This class is intended to become the object to manage orders, including via Order.api.
26 * As of writing it is in the process of having appropriate functions built up.
27 * It should **NOT** be accessed directly outside of tested core methods as it
32 class CRM_Financial_BAO_Order
{
39 protected $priceSetID;
42 * Selected price items in the format we see in forms.
45 * [price_3 => 4, price_10 => 7]
46 * is equivalent to 'option_value 4 for radio price field 3 and
47 * a quantity of 7 for text price field 10.
51 protected $priceSelection = [];
54 * Override for financial type id.
56 * Used when the financial type id is to be overridden for all line items
57 * (as can happen in backoffice forms)
61 protected $overrideFinancialTypeID;
64 * Overridable financial type id.
66 * When this is set only this financial type will be overridden.
68 * This is relevant to repeat transactions where we want to
69 * override the type on the line items if it a different financial type has
70 * been saved against the recurring contribution. However, it the line item
71 * financial type differs from the contribution financial type then we
72 * treat this as deliberately uncoupled and don't flow through changes
73 * in financial type down to the line items.
75 * This is covered in testRepeatTransactionUpdatedFinancialTypeAndNotEquals.
79 protected $overridableFinancialTypeID;
82 * Get overridable financial type id.
84 * If only one financial type id can be overridden at the line item level
85 * then get it here, otherwise NULL.
89 public function getOverridableFinancialTypeID(): ?
int {
90 return $this->overridableFinancialTypeID
;
94 * Set overridable financial type id.
96 * If only one financial type id can be overridden at the line item level
99 * @param int|null $overridableFinancialTypeID
101 public function setOverridableFinancialTypeID(?
int $overridableFinancialTypeID): void
{
102 $this->overridableFinancialTypeID
= $overridableFinancialTypeID;
106 * Financial type id to use for any lines where is is not provided.
110 protected $defaultFinancialTypeID;
113 * ID of a contribution to be used as a template.
117 protected $templateContributionID;
120 * Should we permit the line item financial type to be overridden when there is more than one line.
122 * Historically the answer is 'yes' for v3 order api and 'no' for repeattransaction
123 * and backoffice forms.
127 protected $isPermitOverrideFinancialTypeForMultipleLines = FALSE;
132 public function isPermitOverrideFinancialTypeForMultipleLines(): bool {
133 return $this->isPermitOverrideFinancialTypeForMultipleLines
;
137 * @param bool $isPermitOverrideFinancialTypeForMultipleLines
139 public function setIsPermitOverrideFinancialTypeForMultipleLines(bool $isPermitOverrideFinancialTypeForMultipleLines): void
{
140 $this->isPermitOverrideFinancialTypeForMultipleLines
= $isPermitOverrideFinancialTypeForMultipleLines;
144 * Number of line items.
148 protected $lineItemCount;
153 public function getLineItemCount(): int {
154 if (!isset($this->lineItemCount
)) {
155 $this->lineItemCount
= count($this->getPriceOptions()) ||
count($this->lineItems
);
157 return $this->lineItemCount
;
161 * @param int $lineItemCount
163 public function setLineItemCount(int $lineItemCount): void
{
164 $this->lineItemCount
= $lineItemCount;
170 public function getTemplateContributionID(): ?
int {
171 return $this->templateContributionID
;
175 * @param int $templateContributionID
177 public function setTemplateContributionID(int $templateContributionID): void
{
178 $this->templateContributionID
= $templateContributionID;
184 public function getDefaultFinancialTypeID(): int {
185 return $this->defaultFinancialTypeID
;
189 * Set the default financial type id to be used when the line has none.
191 * @param int|null $defaultFinancialTypeID
193 public function setDefaultFinancialTypeID(?
int $defaultFinancialTypeID): void
{
194 $this->defaultFinancialTypeID
= $defaultFinancialTypeID;
198 * Override for the total amount of the order.
200 * When there is a single line item the order total may be overriden.
204 protected $overrideTotalAmount;
207 * Line items in the order.
211 protected $lineItems = [];
214 * Array of entities ordered.
218 protected $entityParameters = [];
221 * Default price sets for component.
225 protected $defaultPriceSets = [];
228 * Cache of the default price field.
232 protected $defaultPriceField;
235 * Cache of the default price field value ID.
239 protected $defaultPriceFieldValueID;
242 * Get parameters for the entities bought as part of this order.
246 * @internal core tested code only.
249 public function getEntitiesToCreate(): array {
251 foreach ($this->entityParameters
as $entityToCreate) {
252 if (in_array($entityToCreate['entity'], ['participant', 'membership'], TRUE)) {
253 $entities[] = $entityToCreate;
260 * Set parameters for the entities bought as part of this order.
262 * @param array $entityParameters
263 * @param int|string $key indexing reference
265 * @internal core tested code only.
268 public function setEntityParameters(array $entityParameters, $key): void
{
269 $this->entityParameters
[$key] = $entityParameters;
273 * Add a line item to an entity.
275 * The v3 api supports more than on line item being stored against a given
276 * set of entity parameters. There is some doubt as to whether this is a
277 * good thing that should be supported in v4 or something that 'seemed
278 * like a good idea at the time' - but this allows the lines to be added from the
281 * @param string $lineIndex
282 * @param string $entityKey
284 public function addLineItemToEntityParameters(string $lineIndex, string $entityKey): void
{
285 $this->entityParameters
[$entityKey]['entity'] = $this->getLineItemEntity($lineIndex);
286 $this->entityParameters
[$entityKey]['line_references'][] = $lineIndex;
290 * Metadata for price fields.
294 protected $priceFieldMetadata = [];
297 * Metadata for price field values.
301 protected $priceFieldValueMetadata = [];
304 * Metadata for price sets.
308 protected $priceSetMetadata = [];
313 * @internal use in tested core code only.
315 * @return \CRM_Core_Form|NULL
317 public function getForm(): ?CRM_Core_Form
{
324 * @internal use in tested core code only.
326 * @param \CRM_Core_Form|null $form
328 public function setForm(?CRM_Core_Form
$form): void
{
333 * The serialize & unserialize functions are to prevent the form being serialized & stored.
335 * The form could be potentially large & circular.
337 * We simply serialize the values needed to re-serialize the form.
341 public function _serialize(): array {
343 'OverrideTotalAmount' => $this->getOverrideTotalAmount(),
344 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(),
345 'PriceSelection' => $this->getPriceSelection(),
350 * Re-instantiate the the class with non-calculated variables.
354 public function _unserialize(array $data): void
{
355 foreach ($data as $key => $value) {
362 * Form object - if present the buildAmount hook will be called.
364 * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal
369 * Get Set override for total amount of the order.
371 * @internal use in tested core code only.
373 * @return float|false
375 public function getOverrideTotalAmount() {
376 // The override amount is only valid for quick config price sets where more
377 // than one field has not been selected.
378 if (!$this->overrideTotalAmount ||
$this->getLineItemCount() > 1) {
381 return $this->overrideTotalAmount
;
385 * Is the line item financial type to be overridden.
387 * We have a tested scenario for repeatcontribution where the line item
388 * does not match the top level financial type for the order. In this case
389 * any financial type override has been determined to NOT apply to the line items.
391 * This is locked in via testRepeatTransactionUpdatedFinancialTypeAndNotEquals.
393 * @param int $financialTypeID
397 public function isOverrideLineItemFinancialType(int $financialTypeID) {
398 if (!$this->getOverrideFinancialTypeID()) {
401 if (!$this->getOverridableFinancialTypeID()) {
404 return $this->getOverridableFinancialTypeID() === $financialTypeID;
408 * Set override for total amount.
410 * @internal use in tested core code only.
412 * @param float|null $overrideTotalAmount
414 public function setOverrideTotalAmount(?
float $overrideTotalAmount): void
{
415 $this->overrideTotalAmount
= $overrideTotalAmount;
419 * Get override for total amount.
421 * @internal use in tested core code only.
425 public function getOverrideFinancialTypeID() {
426 // We don't permit overrides if there is more than one line.
427 // The reason for this constraint may be more historical since
428 // the case could be made that if it is set it should be used and
429 // we have built out the tax calculations a lot now.
430 if (!$this->isPermitOverrideFinancialTypeForMultipleLines() && $this->getLineItemCount() > 1) {
433 return $this->overrideFinancialTypeID ??
FALSE;
437 * Set override for financial type ID.
439 * @internal use in tested core code only.
441 * @param int|null $overrideFinancialTypeID
443 public function setOverrideFinancialTypeID(?
int $overrideFinancialTypeID): void
{
444 $this->overrideFinancialTypeID
= $overrideFinancialTypeID;
448 * Getter for price set id.
450 * @internal use in tested core code only.
454 * @throws \API_Exception
456 public function getPriceSetID(): int {
457 if (!$this->priceSetID
) {
458 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
459 $this->setPriceSetIDFromSelectedField($fieldID);
461 if (!$this->priceSetID
&& $this->getTemplateContributionID()) {
462 // Load the line items from the contribution.
463 foreach ($this->getLineItems() as $lineItem) {
464 return $lineItem['price_field_id.price_set_id'];
468 return $this->priceSetID
;
472 * Setter for price set id.
474 * @internal use in tested core code only.
476 * @param int $priceSetID
478 public function setPriceSetID(int $priceSetID) {
479 $this->priceSetID
= $priceSetID;
483 * Set price set id to the default.
485 * @param string $component [membership|contribution]
487 * @throws \API_Exception
488 * @internal use in tested core code only.
490 public function setPriceSetToDefault(string $component): void
{
491 $this->priceSetID
= $this->getDefaultPriceSetForComponent($component);
495 * Set price set ID based on the contribution page id.
497 * @internal use in tested core code only.
499 * @param int $contributionPageID
502 public function setPriceSetIDByContributionPageID(int $contributionPageID): void
{
503 $this->setPriceSetIDByEntity('contribution_page', $contributionPageID);
507 * Set price set ID based on the event id.
509 * @internal use in tested core code only.
511 * @param int $eventID
513 * @throws \CiviCRM_API3_Exception
515 public function setPriceSetIDByEventPageID(int $eventID): void
{
516 $this->setPriceSetIDByEntity('event', $eventID);
520 * Set the price set id based on looking up the entity.
522 * @internal use in tested core code only.
524 * @param string $entity
528 protected function setPriceSetIDByEntity(string $entity, int $id): void
{
529 $this->priceSetID
= CRM_Price_BAO_PriceSet
::getFor('civicrm_' . $entity, $id);
533 * Getter for price selection.
535 * @internal use in tested core code only.
539 public function getPriceSelection(): array {
540 return $this->priceSelection
;
544 * Setter for price selection.
546 * @internal use in tested core code only.
548 * @param array $priceSelection
550 public function setPriceSelection(array $priceSelection) {
551 $this->priceSelection
= $priceSelection;
555 * Price options the simplified price fields selections.
557 * ie. the 'price_' is stripped off the key name and the field ID
558 * is cast to an integer.
560 * @internal use in tested core code only.
564 public function getPriceOptions():array {
566 foreach ($this->getPriceSelection() as $fieldName => $value) {
567 $fieldID = substr($fieldName, 6);
568 $priceOptions[(int) $fieldID] = $value;
570 return $priceOptions;
574 * Get the metadata for the given field.
576 * @internal use in tested core code only.
582 public function getPriceFieldSpec(int $id) :array {
583 return $this->getPriceFieldsMetadata()[$id];
587 * Get the metadata for the given field value.
589 * @internal use in tested core code only.
595 public function getPriceFieldValueSpec(int $id) :array {
596 if (!isset($this->priceFieldValueMetadata
[$id])) {
597 $this->priceFieldValueMetadata
[$id] = PriceFieldValue
::get(FALSE)->addWhere('id', '=', $id)->execute()->first();
599 return $this->priceFieldValueMetadata
[$id];
603 * Get the metadata for the fields in the price set.
605 * @internal use in tested core code only.
609 public function getPriceFieldsMetadata(): array {
610 if (empty($this->priceFieldMetadata
)) {
611 $this->getPriceSetMetadata();
613 return $this->priceFieldMetadata
;
617 * Set the metadata for the order.
619 * @param array $metadata
621 protected function setPriceFieldMetadata($metadata) {
622 $this->priceFieldMetadata
= $metadata;
623 if ($this->getForm()) {
624 CRM_Utils_Hook
::buildAmount($this->form
->getFormContext(), $this->form
, $this->priceFieldMetadata
);
629 * Get the metadata for the fields in the price set.
631 * @internal use in tested core code only.
635 public function getPriceSetMetadata(): array {
636 if (empty($this->priceSetMetadata
)) {
637 $priceSetMetadata = CRM_Price_BAO_PriceSet
::getCachedPriceSetDetail($this->getPriceSetID());
638 $this->setPriceFieldMetadata($priceSetMetadata['fields']);
639 unset($priceSetMetadata['fields']);
640 $this->priceSetMetadata
= $priceSetMetadata;
642 return $this->priceSetMetadata
;
646 * Get the financial type id for the order.
648 * @internal use in tested core code only.
650 * This may differ to the line items....
654 public function getFinancialTypeID(): int {
655 return (int) $this->getOverrideFinancialTypeID() ?
: $this->getPriceSetMetadata()['financial_type_id'];
659 * Set the price field selection from an array of params containing price
662 * This function takes the sort of 'anything & everything' parameters that
663 * come in from the form layer and filters them before assigning them to the
664 * priceSelection property.
666 * @param array $input
668 * @throws \API_Exception
670 public function setPriceSelectionFromUnfilteredInput(array $input): void
{
671 foreach ($input as $fieldName => $value) {
672 if (strpos($fieldName, 'price_') === 0) {
673 $fieldID = substr($fieldName, 6);
674 if (is_numeric($fieldID)) {
675 $this->priceSelection
[$fieldName] = $value;
679 if (empty($this->priceSelection
) && isset($input['total_amount'])
680 && is_numeric($input['total_amount']) && !empty($input['financial_type_id'])) {
681 $this->priceSelection
['price_' . $this->getDefaultPriceFieldID()] = $input['total_amount'];
682 $this->setOverrideFinancialTypeID($input['financial_type_id']);
687 * Get the id of the price field to use when just an amount is provided.
689 * @throws \API_Exception
693 public function getDefaultPriceFieldID():int {
694 if (!$this->defaultPriceField
) {
695 $this->defaultPriceField
= PriceField
::get(FALSE)
696 ->addWhere('name', '=', 'contribution_amount')
697 ->addWhere('price_set_id.name', '=', 'default_contribution_amount')
698 ->execute()->first();
700 return $this->defaultPriceField
['id'];
704 * Get the id of the price field to use when just an amount is provided.
706 * @throws \API_Exception
710 public function getDefaultPriceFieldValueID():int {
711 if (!$this->defaultPriceFieldValueID
) {
712 $this->defaultPriceFieldValueID
= PriceFieldValue
::get(FALSE)
713 ->addWhere('name', '=', 'contribution_amount')
714 ->addWhere('price_field_id.name', '=', 'contribution_amount')
715 ->execute()->first()['id'];
717 return $this->defaultPriceFieldValueID
;
725 * @throws \CiviCRM_API3_Exception
727 public function getLineItems():array {
728 if (empty($this->lineItems
)) {
729 $this->lineItems
= $this->calculateLineItems();
731 return $this->lineItems
;
735 * Get line items in a 'traditional' indexing format.
737 * This ensures the line items are indexed by
738 * price field id - as required by the contribution BAO.
740 * @throws \CiviCRM_API3_Exception
742 public function getPriceFieldIndexedLineItems(): array {
744 foreach ($this->getLineItems() as $item) {
745 $lines[$item['price_field_id']] = $item;
751 * Get line items that specifically relate to memberships.
755 * @throws \CiviCRM_API3_Exception
757 public function getMembershipLineItems():array {
758 $lines = $this->getLineItems();
759 foreach ($lines as $index => $line) {
760 if (empty($line['membership_type_id'])) {
761 unset($lines[$index]);
764 if (empty($line['membership_num_terms'])) {
765 $lines[$index]['membership_num_terms'] = 1;
772 * Get an array of all membership types included in the order.
776 * @throws \CiviCRM_API3_Exception
778 public function getMembershipTypes(): array {
780 foreach ($this->getMembershipLineItems() as $line) {
781 $types[$line['membership_type_id']] = CRM_Member_BAO_MembershipType
::getMembershipType((int) $line['membership_type_id']);
787 * Get an array of all membership types included in the order.
791 * @throws \CiviCRM_API3_Exception
793 public function getRenewableMembershipTypes(): array {
795 foreach ($this->getMembershipTypes() as $id => $type) {
796 if (!empty($type['auto_renew'])) {
806 * @throws \API_Exception
808 protected function calculateLineItems(): array {
810 $params = $this->getPriceSelection();
811 if ($this->getOverrideTotalAmount() !== FALSE) {
812 // We need to do this to keep getLine from doing weird stuff but the goal
813 // is to ditch getLine next round of refactoring
814 // and make the code more sane.
815 $params['total_amount'] = $this->getOverrideTotalAmount();
818 // Dummy value to prevent e-notice in getLine. We calculate tax in this class.
819 $params['financial_type_id'] = 0;
820 if ($this->getTemplateContributionID()) {
821 $lineItems = $this->getLinesFromTemplateContribution();
824 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
825 if ($valueID !== '') {
826 $this->setPriceSetIDFromSelectedField($fieldID);
827 $throwAwayArray = [];
828 // @todo - still using getLine for now but better to bring it to this class & do a better job.
829 $newLines = CRM_Price_BAO_PriceSet
::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
830 foreach ($newLines as $newLine) {
831 $lineItems[$newLine['price_field_value_id']] = $newLine;
836 // Set the line item count here because it is needed to determine whether
837 // we can use overrides and would not be set yet if we have loaded them from
838 // a template contribution.
839 $this->setLineItemCount(count($lineItems));
841 foreach ($lineItems as &$lineItem) {
842 // Set the price set id if not set above. Note that the above
843 // requires it for line retrieval but we want to fix that as it
844 // should not be required at that point.
845 $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
846 // Set any pre-calculation to zero as we will calculate.
847 $lineItem['tax_amount'] = 0;
848 if ($this->isOverrideLineItemFinancialType($lineItem['financial_type_id']) !== FALSE) {
849 $lineItem['financial_type_id'] = $this->getOverrideFinancialTypeID();
851 $lineItem['tax_rate'] = $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
852 if ($this->getOverrideTotalAmount() !== FALSE) {
853 $this->addTotalsToLineBasedOnOverrideTotal((int) $lineItem['financial_type_id'], $lineItem);
856 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
858 $lineItem['title'] = $lineItem['label'];
864 * Get the total amount for the order.
868 * @throws \CiviCRM_API3_Exception
870 public function getTotalTaxAmount() :float {
872 foreach ($this->getLineItems() as $lineItem) {
873 $amount +
= $lineItem['tax_amount'] ??
0.0;
879 * Get the total amount for the order.
883 * @throws \CiviCRM_API3_Exception
885 public function getTotalAmount() :float {
887 foreach ($this->getLineItems() as $lineItem) {
888 $amount +
= ($lineItem['line_total'] ??
0.0) +
($lineItem['tax_amount'] ??
0.0);
894 * Get the total amount relating to memberships for the order.
898 * @throws \CiviCRM_API3_Exception
900 public function getMembershipTotalAmount() :float {
902 foreach ($this->getMembershipLineItems() as $lineItem) {
903 $amount +
= ($lineItem['line_total'] ??
0.0) +
($lineItem['tax_amount'] ??
0.0);
909 * Get the tax rate for the given financial type.
911 * @param int $financialTypeID
915 public function getTaxRate(int $financialTypeID) {
916 $taxRates = CRM_Core_PseudoConstant
::getTaxRates();
917 if (!isset($taxRates[$financialTypeID])) {
920 return $taxRates[$financialTypeID];
926 * @throws \API_Exception
928 protected function setPriceSetIDFromSelectedField($fieldID): void
{
929 if (!isset($this->priceSetID
)) {
930 $this->setPriceSetID(PriceField
::get(FALSE)
931 ->addSelect('price_set_id')
932 ->addWhere('id', '=', $fieldID)
934 ->first()['price_set_id']);
941 * This function augments the line item where possible. The calling code
942 * should not attempt to set taxes. This function allows minimal values
943 * to be passed for the default price sets - ie if only membership_type_id is
944 * specified the price_field_id and price_value_id will be determined.
946 * @param array $lineItem
947 * @param int|string $index
949 * @throws \API_Exception
950 * @internal tested core code usage only.
951 * @internal use in tested core code only.
954 public function setLineItem(array $lineItem, $index): void
{
955 if (!isset($this->priceSetID
)) {
956 if (!empty($lineItem['price_field_id'])) {
957 $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
960 // we are using either the default membership or default contribution
961 // If membership type is passed in we use the default price field.
962 $component = !empty($lineItem['membership_type_id']) ?
'membership' : 'contribution';
963 $this->setPriceSetToDefault($component);
966 if (!isset($lineItem['financial_type_id'])) {
967 $lineItem['financial_type_id'] = $this->getDefaultFinancialTypeID();
969 if (!is_numeric($lineItem['financial_type_id'])) {
970 $lineItem['financial_type_id'] = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', $lineItem['financial_type_id']);
972 if ($this->getOverrideTotalAmount()) {
973 $this->addTotalsToLineBasedOnOverrideTotal((int) $lineItem['financial_type_id'], $lineItem);
976 $lineItem['tax_rate'] = $this->getTaxRate($lineItem['financial_type_id']);
977 $lineItem['tax_amount'] = ($lineItem['tax_rate'] / 100) * $lineItem['line_total'];
979 if (!empty($lineItem['membership_type_id'])) {
980 $lineItem['entity_table'] = 'civicrm_membership';
981 if (empty($lineItem['price_field_id']) && empty($lineItem['price_field_value_id'])) {
982 $lineItem = $this->fillMembershipLine($lineItem);
985 if ($this->getPriceSetID() === $this->getDefaultPriceSetForComponent('contribution')) {
986 $this->fillDefaultContributionLine($lineItem);
988 if (empty($lineItem['label'])) {
989 $lineItem['label'] = PriceFieldValue
::get(FALSE)->addWhere('id', '=', (int) $lineItem['price_field_value_id'])->addSelect('label')->execute()->first()['label'];
991 if (empty($lineItem['price_field_id']) && !empty($lineItem['membership_type_id'])) {
992 // We have to 'guess' the price field since the calling code hasn't
993 // passed it in (which it really should but ... history).
994 foreach ($this->priceFieldMetadata
as $pricefield) {
995 foreach ($pricefield['options'] ??
[] as $option) {
996 if ((int) $option['membership_type_id'] === $lineItem['membership_type_id']) {
997 $lineItem['price_field_id'] = $pricefield['id'];
998 $lineItem['price_field_value_id'] = $option['id'];
1003 if (empty($lineItem['title'])) {
1004 // Title is used in output for workflow templates.
1005 $htmlType = !empty($this->priceFieldMetadata
) ?
$this->getPriceFieldSpec($lineItem['price_field_id'])['html_type'] : NULL;
1006 $lineItem['title'] = (!$htmlType ||
$htmlType === 'Text') ?
$lineItem['label'] : $this->getPriceFieldSpec($lineItem['price_field_id'])['label'] . ' : ' . $lineItem['label'];
1007 if (!empty($lineItem['price_field_value_id'])) {
1008 $description = $this->priceFieldValueMetadata
[$lineItem['price_field_value_id']]['description'] ??
'';
1010 $lineItem['title'] .= ' ' . CRM_Utils_String
::ellipsify($description, 30);
1014 $this->lineItems
[$index] = $lineItem;
1018 * Set a value on a line item.
1020 * @internal only use in core tested code.
1022 * @param string $name
1023 * @param mixed $value
1024 * @param string|int $index
1026 public function setLineItemValue(string $name, $value, $index): void
{
1027 $this->lineItems
[$index][$name] = $value;
1031 * @param int|string $index
1035 public function getLineItemEntity($index):string {
1036 // @todo - ensure entity_table is set in setLineItem, go back to enotices here.
1037 return str_replace('civicrm_', '', ($this->lineItems
[$index]['entity_table'] ??
'contribution'));
1041 * Get the ordered line item.
1043 * @param string|int $index
1047 public function getLineItem($index): array {
1048 return $this->lineItems
[$index];
1052 * Fills in additional data for the membership line.
1054 * The minimum requirement is the membership_type_id and that priceSetID is set.
1056 * @param array $lineItem
1060 protected function fillMembershipLine(array $lineItem): array {
1061 $fields = $this->getPriceFieldsMetadata();
1062 foreach ($fields as $field) {
1063 if (!isset($lineItem['price_field_value_id'])) {
1064 foreach ($field['options'] as $option) {
1065 if ((int) $option['membership_type_id'] === (int) $lineItem['membership_type_id']) {
1066 $lineItem['price_field_id'] = $field['id'];
1067 $lineItem['price_field_id.label'] = $field['label'];
1068 $lineItem['price_field_value_id'] = $option['id'];
1069 $lineItem['qty'] = 1;
1073 if (isset($lineItem['price_field_value_id'], $field['options'][$lineItem['price_field_value_id']])) {
1074 $option = $field['options'][$lineItem['price_field_value_id']];
1077 $lineItem['unit_price'] = $lineItem['line_total'] ??
$option['amount'];
1078 $lineItem['label'] = $lineItem['label'] ??
$option['label'];
1079 $lineItem['field_title'] = $lineItem['field_title'] ??
$option['label'];
1080 $lineItem['financial_type_id'] = $lineItem['financial_type_id'] ?
: ($this->getDefaultFinancialTypeID() ??
$option['financial_type_id']);
1085 * Add total_amount and tax_amount to the line from the override total.
1087 * @param int $financialTypeID
1088 * @param array $lineItem
1092 protected function addTotalsToLineBasedOnOverrideTotal(int $financialTypeID, array &$lineItem): void
{
1093 $lineItem['tax_rate'] = $taxRate = $this->getTaxRate($financialTypeID);
1095 // Total is tax inclusive.
1096 $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 +
($taxRate / 100));
1097 $lineItem['line_total'] = $this->getOverrideTotalAmount() - $lineItem['tax_amount'];
1100 $lineItem['line_total'] = $this->getOverrideTotalAmount();
1102 if (!empty($lineItem['qty'])) {
1103 $lineItem['unit_price'] = $lineItem['line_total'] / $lineItem['qty'];
1106 $lineItem['unit_price'] = $lineItem['line_total'];
1111 * Get the line items from a template.
1113 * @return \Civi\Api4\Generic\Result
1115 * @throws \API_Exception
1117 protected function getLinesFromTemplateContribution(): array {
1118 $lines = $this->getLinesForContribution();
1119 foreach ($lines as &$line) {
1120 // The apiv4 insists on adding id - so let it get all the details
1121 // and we will filter out those that are not part of a template here.
1122 unset($line['id'], $line['contribution_id']);
1128 * Get the constructed line items formatted for the v3 Order api.
1132 * @internal core tested code only.
1134 * @throws \CiviCRM_API3_Exception
1136 public function getLineItemForV3OrderApi(): array {
1138 foreach ($this->getLineItems() as $key => $line) {
1140 'line_item' => [$line['price_field_value_id'] => $line],
1141 'params' => $this->entityParameters
[$key] ??
[],
1149 * @throws \API_Exception
1150 * @throws \Civi\API\Exception\UnauthorizedException
1152 protected function getLinesForContribution(): array {
1153 return (array) LineItem
::get(FALSE)
1154 ->addWhere('contribution_id', '=', $this->getTemplateContributionID())
1160 'price_field_id.label',
1161 'price_field_id.price_set_id',
1162 'price_field_value_id',
1163 'financial_type_id',
1169 'non_deductible_amount',
1170 'participant_count',
1171 'membership_num_terms',
1177 * Get the default price set id for the given component.
1179 * @param string $component
1182 * @throws \API_Exception
1184 protected function getDefaultPriceSetForComponent(string $component): int {
1185 if (!isset($this->defaultPriceSets
[$component])) {
1186 $this->defaultPriceSets
[$component] = PriceSet
::get(FALSE)
1187 ->addWhere('name', '=', ($component === 'membership' ?
'default_membership_type_amount' : 'default_contribution_amount'))
1191 return $this->defaultPriceSets
[$component];
1195 * Fill in values for a default contribution line item.
1197 * @param array $lineItem
1199 * @throws \API_Exception
1201 protected function fillDefaultContributionLine(array &$lineItem): void
{
1204 'price_field_id' => $this->getDefaultPriceFieldID(),
1205 'price_field_id.label' => $this->defaultPriceField
['label'],
1206 'price_field_value_id' => $this->getDefaultPriceFieldValueID(),
1207 'entity_table' => 'civicrm_contribution',
1208 'unit_price' => $lineItem['line_total'],
1209 'label' => ts('Contribution Amount'),
1211 $lineItem = array_merge($defaults, $lineItem);