cache key
[civicrm-core.git] / CRM / Financial / BAO / Order.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\LineItem;
13 use Civi\Api4\PriceField;
14 use Civi\Api4\PriceFieldValue;
15 use Civi\Api4\PriceSet;
16
17 /**
18 *
19 * @package CRM
20 * @copyright CiviCRM LLC https://civicrm.org/licensing
21 *
22 * Order class.
23 *
24 * This class is intended to become the object to manage orders, including via Order.api.
25 *
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
28 * may change.
29 *
30 * @internal
31 */
32 class CRM_Financial_BAO_Order {
33
34 /**
35 * Price set id.
36 *
37 * @var int
38 */
39 protected $priceSetID;
40
41 /**
42 * Selected price items in the format we see in forms.
43 *
44 * ie.
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.
48 *
49 * @var array
50 */
51 protected $priceSelection = [];
52
53 /**
54 * Override for financial type id.
55 *
56 * Used when the financial type id is to be overridden for all line items
57 * (as can happen in backoffice forms)
58 *
59 * @var int
60 */
61 protected $overrideFinancialTypeID;
62
63 /**
64 * Overridable financial type id.
65 *
66 * When this is set only this financial type will be overridden.
67 *
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.
74 *
75 * This is covered in testRepeatTransactionUpdatedFinancialTypeAndNotEquals.
76 *
77 * @var int
78 */
79 protected $overridableFinancialTypeID;
80
81 /**
82 * Get overridable financial type id.
83 *
84 * If only one financial type id can be overridden at the line item level
85 * then get it here, otherwise NULL.
86 *
87 * @return int|null
88 */
89 public function getOverridableFinancialTypeID(): ?int {
90 return $this->overridableFinancialTypeID;
91 }
92
93 /**
94 * Set overridable financial type id.
95 *
96 * If only one financial type id can be overridden at the line item level
97 * then set it here.
98 *
99 * @param int|null $overridableFinancialTypeID
100 */
101 public function setOverridableFinancialTypeID(?int $overridableFinancialTypeID): void {
102 $this->overridableFinancialTypeID = $overridableFinancialTypeID;
103 }
104
105 /**
106 * Financial type id to use for any lines where is is not provided.
107 *
108 * @var int
109 */
110 protected $defaultFinancialTypeID;
111
112 /**
113 * ID of a contribution to be used as a template.
114 *
115 * @var int
116 */
117 protected $templateContributionID;
118
119 /**
120 * Should we permit the line item financial type to be overridden when there is more than one line.
121 *
122 * Historically the answer is 'yes' for v3 order api and 'no' for repeattransaction
123 * and backoffice forms.
124 *
125 * @var bool
126 */
127 protected $isPermitOverrideFinancialTypeForMultipleLines = FALSE;
128
129 /**
130 * @return bool
131 */
132 public function isPermitOverrideFinancialTypeForMultipleLines(): bool {
133 return $this->isPermitOverrideFinancialTypeForMultipleLines;
134 }
135
136 /**
137 * @param bool $isPermitOverrideFinancialTypeForMultipleLines
138 */
139 public function setIsPermitOverrideFinancialTypeForMultipleLines(bool $isPermitOverrideFinancialTypeForMultipleLines): void {
140 $this->isPermitOverrideFinancialTypeForMultipleLines = $isPermitOverrideFinancialTypeForMultipleLines;
141 }
142
143 /**
144 * Number of line items.
145 *
146 * @var int
147 */
148 protected $lineItemCount;
149
150 /**
151 * @return int
152 */
153 public function getLineItemCount(): int {
154 if (!isset($this->lineItemCount)) {
155 $this->lineItemCount = count($this->getPriceOptions()) || count($this->lineItems);
156 }
157 return $this->lineItemCount;
158 }
159
160 /**
161 * @param int $lineItemCount
162 */
163 public function setLineItemCount(int $lineItemCount): void {
164 $this->lineItemCount = $lineItemCount;
165 }
166
167 /**
168 * @return int|null
169 */
170 public function getTemplateContributionID(): ?int {
171 return $this->templateContributionID;
172 }
173
174 /**
175 * @param int $templateContributionID
176 */
177 public function setTemplateContributionID(int $templateContributionID): void {
178 $this->templateContributionID = $templateContributionID;
179 }
180
181 /**
182 * @return int
183 */
184 public function getDefaultFinancialTypeID(): int {
185 return $this->defaultFinancialTypeID;
186 }
187
188 /**
189 * Set the default financial type id to be used when the line has none.
190 *
191 * @param int|null $defaultFinancialTypeID
192 */
193 public function setDefaultFinancialTypeID(?int $defaultFinancialTypeID): void {
194 $this->defaultFinancialTypeID = $defaultFinancialTypeID;
195 }
196
197 /**
198 * Override for the total amount of the order.
199 *
200 * When there is a single line item the order total may be overriden.
201 *
202 * @var float
203 */
204 protected $overrideTotalAmount;
205
206 /**
207 * Line items in the order.
208 *
209 * @var array
210 */
211 protected $lineItems = [];
212
213 /**
214 * Array of entities ordered.
215 *
216 * @var array
217 */
218 protected $entityParameters = [];
219
220 /**
221 * Default price sets for component.
222 *
223 * @var array
224 */
225 protected $defaultPriceSets = [];
226
227 /**
228 * Cache of the default price field.
229 *
230 * @var array
231 */
232 protected $defaultPriceField;
233
234 /**
235 * Cache of the default price field value ID.
236 *
237 * @var array
238 */
239 protected $defaultPriceFieldValueID;
240
241 /**
242 * Get parameters for the entities bought as part of this order.
243 *
244 * @return array
245 *
246 * @internal core tested code only.
247 *
248 */
249 public function getEntitiesToCreate(): array {
250 $entities = [];
251 foreach ($this->entityParameters as $entityToCreate) {
252 if (in_array($entityToCreate['entity'], ['participant', 'membership'], TRUE)) {
253 $entities[] = $entityToCreate;
254 }
255 }
256 return $entities;
257 }
258
259 /**
260 * Set parameters for the entities bought as part of this order.
261 *
262 * @param array $entityParameters
263 * @param int|string $key indexing reference
264 *
265 * @internal core tested code only.
266 *
267 */
268 public function setEntityParameters(array $entityParameters, $key): void {
269 $this->entityParameters[$key] = $entityParameters;
270 }
271
272 /**
273 * Add a line item to an entity.
274 *
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
279 * v3 api.
280 *
281 * @param string $lineIndex
282 * @param string $entityKey
283 */
284 public function addLineItemToEntityParameters(string $lineIndex, string $entityKey): void {
285 $this->entityParameters[$entityKey]['entity'] = $this->getLineItemEntity($lineIndex);
286 $this->entityParameters[$entityKey]['line_references'][] = $lineIndex;
287 }
288
289 /**
290 * Metadata for price fields.
291 *
292 * @var array
293 */
294 protected $priceFieldMetadata = [];
295
296 /**
297 * Metadata for price field values.
298 *
299 * @var array
300 */
301 protected $priceFieldValueMetadata = [];
302
303 /**
304 * Metadata for price sets.
305 *
306 * @var array
307 */
308 protected $priceSetMetadata = [];
309
310 /**
311 * Get form object.
312 *
313 * @internal use in tested core code only.
314 *
315 * @return \CRM_Core_Form|NULL
316 */
317 public function getForm(): ?CRM_Core_Form {
318 return $this->form;
319 }
320
321 /**
322 * Set form object.
323 *
324 * @internal use in tested core code only.
325 *
326 * @param \CRM_Core_Form|null $form
327 */
328 public function setForm(?CRM_Core_Form $form): void {
329 $this->form = $form;
330 }
331
332 /**
333 * The serialize & unserialize functions are to prevent the form being serialized & stored.
334 *
335 * The form could be potentially large & circular.
336 *
337 * We simply serialize the values needed to re-serialize the form.
338 *
339 * @return array
340 */
341 public function _serialize(): array {
342 return [
343 'OverrideTotalAmount' => $this->getOverrideTotalAmount(),
344 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(),
345 'PriceSelection' => $this->getPriceSelection(),
346 ];
347 }
348
349 /**
350 * Re-instantiate the the class with non-calculated variables.
351 *
352 * @param array $data
353 */
354 public function _unserialize(array $data): void {
355 foreach ($data as $key => $value) {
356 $fn = 'set' . $key;
357 $this->$fn($value);
358 }
359 }
360
361 /**
362 * Form object - if present the buildAmount hook will be called.
363 *
364 * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal
365 */
366 protected $form;
367
368 /**
369 * Get Set override for total amount of the order.
370 *
371 * @internal use in tested core code only.
372 *
373 * @return float|false
374 */
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) {
379 return FALSE;
380 }
381 return $this->overrideTotalAmount;
382 }
383
384 /**
385 * Is the line item financial type to be overridden.
386 *
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.
390 *
391 * This is locked in via testRepeatTransactionUpdatedFinancialTypeAndNotEquals.
392 *
393 * @param int $financialTypeID
394 *
395 * @return bool
396 */
397 public function isOverrideLineItemFinancialType(int $financialTypeID) {
398 if (!$this->getOverrideFinancialTypeID()) {
399 return FALSE;
400 }
401 if (!$this->getOverridableFinancialTypeID()) {
402 return TRUE;
403 }
404 return $this->getOverridableFinancialTypeID() === $financialTypeID;
405 }
406
407 /**
408 * Set override for total amount.
409 *
410 * @internal use in tested core code only.
411 *
412 * @param float|null $overrideTotalAmount
413 */
414 public function setOverrideTotalAmount(?float $overrideTotalAmount): void {
415 $this->overrideTotalAmount = $overrideTotalAmount;
416 }
417
418 /**
419 * Get override for total amount.
420 *
421 * @internal use in tested core code only.
422 *
423 * @return int| FALSE
424 */
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) {
431 return FALSE;
432 }
433 return $this->overrideFinancialTypeID ?? FALSE;
434 }
435
436 /**
437 * Set override for financial type ID.
438 *
439 * @internal use in tested core code only.
440 *
441 * @param int|null $overrideFinancialTypeID
442 */
443 public function setOverrideFinancialTypeID(?int $overrideFinancialTypeID): void {
444 $this->overrideFinancialTypeID = $overrideFinancialTypeID;
445 }
446
447 /**
448 * Getter for price set id.
449 *
450 * @internal use in tested core code only.
451 *
452 * @return int
453 *
454 * @throws \API_Exception
455 */
456 public function getPriceSetID(): int {
457 if (!$this->priceSetID) {
458 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
459 $this->setPriceSetIDFromSelectedField($fieldID);
460 }
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'];
465 }
466 }
467 }
468 return $this->priceSetID;
469 }
470
471 /**
472 * Setter for price set id.
473 *
474 * @internal use in tested core code only.
475 *
476 * @param int $priceSetID
477 */
478 public function setPriceSetID(int $priceSetID) {
479 $this->priceSetID = $priceSetID;
480 }
481
482 /**
483 * Set price set id to the default.
484 *
485 * @param string $component [membership|contribution]
486 *
487 * @throws \API_Exception
488 * @internal use in tested core code only.
489 */
490 public function setPriceSetToDefault(string $component): void {
491 $this->priceSetID = $this->getDefaultPriceSetForComponent($component);
492 }
493
494 /**
495 * Set price set ID based on the contribution page id.
496 *
497 * @internal use in tested core code only.
498 *
499 * @param int $contributionPageID
500 *
501 */
502 public function setPriceSetIDByContributionPageID(int $contributionPageID): void {
503 $this->setPriceSetIDByEntity('contribution_page', $contributionPageID);
504 }
505
506 /**
507 * Set price set ID based on the event id.
508 *
509 * @internal use in tested core code only.
510 *
511 * @param int $eventID
512 *
513 * @throws \CiviCRM_API3_Exception
514 */
515 public function setPriceSetIDByEventPageID(int $eventID): void {
516 $this->setPriceSetIDByEntity('event', $eventID);
517 }
518
519 /**
520 * Set the price set id based on looking up the entity.
521 *
522 * @internal use in tested core code only.
523 *
524 * @param string $entity
525 * @param int $id
526 *
527 */
528 protected function setPriceSetIDByEntity(string $entity, int $id): void {
529 $this->priceSetID = CRM_Price_BAO_PriceSet::getFor('civicrm_' . $entity, $id);
530 }
531
532 /**
533 * Getter for price selection.
534 *
535 * @internal use in tested core code only.
536 *
537 * @return array
538 */
539 public function getPriceSelection(): array {
540 return $this->priceSelection;
541 }
542
543 /**
544 * Setter for price selection.
545 *
546 * @internal use in tested core code only.
547 *
548 * @param array $priceSelection
549 */
550 public function setPriceSelection(array $priceSelection) {
551 $this->priceSelection = $priceSelection;
552 }
553
554 /**
555 * Price options the simplified price fields selections.
556 *
557 * ie. the 'price_' is stripped off the key name and the field ID
558 * is cast to an integer.
559 *
560 * @internal use in tested core code only.
561 *
562 * @return array
563 */
564 public function getPriceOptions():array {
565 $priceOptions = [];
566 foreach ($this->getPriceSelection() as $fieldName => $value) {
567 $fieldID = substr($fieldName, 6);
568 $priceOptions[(int) $fieldID] = $value;
569 }
570 return $priceOptions;
571 }
572
573 /**
574 * Get the metadata for the given field.
575 *
576 * @internal use in tested core code only.
577 *
578 * @param int $id
579 *
580 * @return array
581 */
582 public function getPriceFieldSpec(int $id) :array {
583 return $this->getPriceFieldsMetadata()[$id];
584 }
585
586 /**
587 * Get the metadata for the given field value.
588 *
589 * @internal use in tested core code only.
590 *
591 * @param int $id
592 *
593 * @return array
594 */
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();
598 }
599 return $this->priceFieldValueMetadata[$id];
600 }
601
602 /**
603 * Get the metadata for the fields in the price set.
604 *
605 * @internal use in tested core code only.
606 *
607 * @return array
608 */
609 public function getPriceFieldsMetadata(): array {
610 if (empty($this->priceFieldMetadata)) {
611 $this->getPriceSetMetadata();
612 }
613 return $this->priceFieldMetadata;
614 }
615
616 /**
617 * Set the metadata for the order.
618 *
619 * @param array $metadata
620 */
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);
625 }
626 }
627
628 /**
629 * Get the metadata for the fields in the price set.
630 *
631 * @internal use in tested core code only.
632 *
633 * @return array
634 */
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;
641 }
642 return $this->priceSetMetadata;
643 }
644
645 /**
646 * Get the financial type id for the order.
647 *
648 * @internal use in tested core code only.
649 *
650 * This may differ to the line items....
651 *
652 * @return int
653 */
654 public function getFinancialTypeID(): int {
655 return (int) $this->getOverrideFinancialTypeID() ?: $this->getPriceSetMetadata()['financial_type_id'];
656 }
657
658 /**
659 * Set the price field selection from an array of params containing price
660 * fields.
661 *
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.
665 *
666 * @param array $input
667 *
668 * @throws \API_Exception
669 */
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;
676 }
677 }
678 }
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']);
683 }
684 }
685
686 /**
687 * Get the id of the price field to use when just an amount is provided.
688 *
689 * @throws \API_Exception
690 *
691 * @return int
692 */
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();
699 }
700 return $this->defaultPriceField['id'];
701 }
702
703 /**
704 * Get the id of the price field to use when just an amount is provided.
705 *
706 * @throws \API_Exception
707 *
708 * @return int
709 */
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'];
716 }
717 return $this->defaultPriceFieldValueID;
718 }
719
720 /**
721 * Get line items.
722 *
723 * return array
724 *
725 * @throws \CiviCRM_API3_Exception
726 */
727 public function getLineItems():array {
728 if (empty($this->lineItems)) {
729 $this->lineItems = $this->calculateLineItems();
730 }
731 return $this->lineItems;
732 }
733
734 /**
735 * Get line items in a 'traditional' indexing format.
736 *
737 * This ensures the line items are indexed by
738 * price field id - as required by the contribution BAO.
739 *
740 * @throws \CiviCRM_API3_Exception
741 */
742 public function getPriceFieldIndexedLineItems(): array {
743 $lines = [];
744 foreach ($this->getLineItems() as $item) {
745 $lines[$item['price_field_id']] = $item;
746 }
747 return $lines;
748 }
749
750 /**
751 * Get line items that specifically relate to memberships.
752 *
753 * return array
754 *
755 * @throws \CiviCRM_API3_Exception
756 */
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]);
762 continue;
763 }
764 if (empty($line['membership_num_terms'])) {
765 $lines[$index]['membership_num_terms'] = 1;
766 }
767 }
768 return $lines;
769 }
770
771 /**
772 * Get an array of all membership types included in the order.
773 *
774 * @return array
775 *
776 * @throws \CiviCRM_API3_Exception
777 */
778 public function getMembershipTypes(): array {
779 $types = [];
780 foreach ($this->getMembershipLineItems() as $line) {
781 $types[$line['membership_type_id']] = CRM_Member_BAO_MembershipType::getMembershipType((int) $line['membership_type_id']);
782 }
783 return $types;
784 }
785
786 /**
787 * Get an array of all membership types included in the order.
788 *
789 * @return array
790 *
791 * @throws \CiviCRM_API3_Exception
792 */
793 public function getRenewableMembershipTypes(): array {
794 $types = [];
795 foreach ($this->getMembershipTypes() as $id => $type) {
796 if (!empty($type['auto_renew'])) {
797 $types[$id] = $type;
798 }
799 }
800 return $types;
801 }
802
803 /**
804 * @return array
805 *
806 * @throws \API_Exception
807 */
808 protected function calculateLineItems(): array {
809 $lineItems = [];
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();
816 }
817
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();
822 }
823 else {
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;
832 }
833 }
834 }
835 }
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));
840
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();
850 }
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);
854 }
855 elseif ($taxRate) {
856 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
857 }
858 $lineItem['title'] = $lineItem['label'];
859 }
860 return $lineItems;
861 }
862
863 /**
864 * Get the total amount for the order.
865 *
866 * @return float
867 *
868 * @throws \CiviCRM_API3_Exception
869 */
870 public function getTotalTaxAmount() :float {
871 $amount = 0.0;
872 foreach ($this->getLineItems() as $lineItem) {
873 $amount += $lineItem['tax_amount'] ?? 0.0;
874 }
875 return $amount;
876 }
877
878 /**
879 * Get the total amount for the order.
880 *
881 * @return float
882 *
883 * @throws \CiviCRM_API3_Exception
884 */
885 public function getTotalAmount() :float {
886 $amount = 0.0;
887 foreach ($this->getLineItems() as $lineItem) {
888 $amount += ($lineItem['line_total'] ?? 0.0) + ($lineItem['tax_amount'] ?? 0.0);
889 }
890 return $amount;
891 }
892
893 /**
894 * Get the total amount relating to memberships for the order.
895 *
896 * @return float
897 *
898 * @throws \CiviCRM_API3_Exception
899 */
900 public function getMembershipTotalAmount() :float {
901 $amount = 0.0;
902 foreach ($this->getMembershipLineItems() as $lineItem) {
903 $amount += ($lineItem['line_total'] ?? 0.0) + ($lineItem['tax_amount'] ?? 0.0);
904 }
905 return $amount;
906 }
907
908 /**
909 * Get the tax rate for the given financial type.
910 *
911 * @param int $financialTypeID
912 *
913 * @return float
914 */
915 public function getTaxRate(int $financialTypeID) {
916 $taxRates = CRM_Core_PseudoConstant::getTaxRates();
917 if (!isset($taxRates[$financialTypeID])) {
918 return 0;
919 }
920 return $taxRates[$financialTypeID];
921 }
922
923 /**
924 * @param $fieldID
925 *
926 * @throws \API_Exception
927 */
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)
933 ->execute()
934 ->first()['price_set_id']);
935 }
936 }
937
938 /**
939 * Set the line item.
940 *
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.
945 *
946 * @param array $lineItem
947 * @param int|string $index
948 *
949 * @throws \API_Exception
950 * @internal tested core code usage only.
951 * @internal use in tested core code only.
952 *
953 */
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']);
958 }
959 else {
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);
964 }
965 }
966 if (!isset($lineItem['financial_type_id'])) {
967 $lineItem['financial_type_id'] = $this->getDefaultFinancialTypeID();
968 }
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']);
971 }
972 if ($this->getOverrideTotalAmount()) {
973 $this->addTotalsToLineBasedOnOverrideTotal((int) $lineItem['financial_type_id'], $lineItem);
974 }
975 else {
976 $lineItem['tax_rate'] = $this->getTaxRate($lineItem['financial_type_id']);
977 $lineItem['tax_amount'] = ($lineItem['tax_rate'] / 100) * $lineItem['line_total'];
978 }
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);
983 }
984 }
985 if ($this->getPriceSetID() === $this->getDefaultPriceSetForComponent('contribution')) {
986 $this->fillDefaultContributionLine($lineItem);
987 }
988 if (empty($lineItem['label'])) {
989 $lineItem['label'] = PriceFieldValue::get(FALSE)->addWhere('id', '=', (int) $lineItem['price_field_value_id'])->addSelect('label')->execute()->first()['label'];
990 }
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'];
999 }
1000 }
1001 }
1002 }
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'] ?? '';
1009 if ($description) {
1010 $lineItem['title'] .= ' ' . CRM_Utils_String::ellipsify($description, 30);
1011 }
1012 }
1013 }
1014 $this->lineItems[$index] = $lineItem;
1015 }
1016
1017 /**
1018 * Set a value on a line item.
1019 *
1020 * @internal only use in core tested code.
1021 *
1022 * @param string $name
1023 * @param mixed $value
1024 * @param string|int $index
1025 */
1026 public function setLineItemValue(string $name, $value, $index): void {
1027 $this->lineItems[$index][$name] = $value;
1028 }
1029
1030 /**
1031 * @param int|string $index
1032 *
1033 * @return string
1034 */
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'));
1038 }
1039
1040 /**
1041 * Get the ordered line item.
1042 *
1043 * @param string|int $index
1044 *
1045 * @return array
1046 */
1047 public function getLineItem($index): array {
1048 return $this->lineItems[$index];
1049 }
1050
1051 /**
1052 * Fills in additional data for the membership line.
1053 *
1054 * The minimum requirement is the membership_type_id and that priceSetID is set.
1055 *
1056 * @param array $lineItem
1057 *
1058 * @return array
1059 */
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;
1070 }
1071 }
1072 }
1073 if (isset($lineItem['price_field_value_id'], $field['options'][$lineItem['price_field_value_id']])) {
1074 $option = $field['options'][$lineItem['price_field_value_id']];
1075 }
1076 }
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']);
1081 return $lineItem;
1082 }
1083
1084 /**
1085 * Add total_amount and tax_amount to the line from the override total.
1086 *
1087 * @param int $financialTypeID
1088 * @param array $lineItem
1089 *
1090 * @return void
1091 */
1092 protected function addTotalsToLineBasedOnOverrideTotal(int $financialTypeID, array &$lineItem): void {
1093 $lineItem['tax_rate'] = $taxRate = $this->getTaxRate($financialTypeID);
1094 if ($taxRate) {
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'];
1098 }
1099 else {
1100 $lineItem['line_total'] = $this->getOverrideTotalAmount();
1101 }
1102 if (!empty($lineItem['qty'])) {
1103 $lineItem['unit_price'] = $lineItem['line_total'] / $lineItem['qty'];
1104 }
1105 else {
1106 $lineItem['unit_price'] = $lineItem['line_total'];
1107 }
1108 }
1109
1110 /**
1111 * Get the line items from a template.
1112 *
1113 * @return \Civi\Api4\Generic\Result
1114 *
1115 * @throws \API_Exception
1116 */
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']);
1123 }
1124 return $lines;
1125 }
1126
1127 /**
1128 * Get the constructed line items formatted for the v3 Order api.
1129 *
1130 * @return array
1131 *
1132 * @internal core tested code only.
1133 *
1134 * @throws \CiviCRM_API3_Exception
1135 */
1136 public function getLineItemForV3OrderApi(): array {
1137 $lineItems = [];
1138 foreach ($this->getLineItems() as $key => $line) {
1139 $lineItems[] = [
1140 'line_item' => [$line['price_field_value_id'] => $line],
1141 'params' => $this->entityParameters[$key] ?? [],
1142 ];
1143 }
1144 return $lineItems;
1145 }
1146
1147 /**
1148 * @return array
1149 * @throws \API_Exception
1150 * @throws \Civi\API\Exception\UnauthorizedException
1151 */
1152 protected function getLinesForContribution(): array {
1153 return (array) LineItem::get(FALSE)
1154 ->addWhere('contribution_id', '=', $this->getTemplateContributionID())
1155 ->setSelect([
1156 'contribution_id',
1157 'entity_id',
1158 'entity_table',
1159 'price_field_id',
1160 'price_field_id.label',
1161 'price_field_id.price_set_id',
1162 'price_field_value_id',
1163 'financial_type_id',
1164 'label',
1165 'qty',
1166 'unit_price',
1167 'line_total',
1168 'tax_amount',
1169 'non_deductible_amount',
1170 'participant_count',
1171 'membership_num_terms',
1172 ])
1173 ->execute();
1174 }
1175
1176 /**
1177 * Get the default price set id for the given component.
1178 *
1179 * @param string $component
1180 *
1181 * @return int
1182 * @throws \API_Exception
1183 */
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'))
1188 ->execute()
1189 ->first()['id'];
1190 }
1191 return $this->defaultPriceSets[$component];
1192 }
1193
1194 /**
1195 * Fill in values for a default contribution line item.
1196 *
1197 * @param array $lineItem
1198 *
1199 * @throws \API_Exception
1200 */
1201 protected function fillDefaultContributionLine(array &$lineItem): void {
1202 $defaults = [
1203 'qty' => 1,
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'),
1210 ];
1211 $lineItem = array_merge($defaults, $lineItem);
1212 }
1213
1214 }