Merge pull request #22152 from eileenmcnaughton/n1
[civicrm-core.git] / CRM / Financial / BAO / Order.php
CommitLineData
7aa78908 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
4df23f2a 12use Civi\Api4\LineItem;
58315149 13use Civi\Api4\PriceField;
60e5cf34 14use Civi\Api4\PriceFieldValue;
812b2c0c 15use Civi\Api4\PriceSet;
58315149 16
7aa78908 17/**
18 *
19 * @package CRM
20 * @copyright CiviCRM LLC https://civicrm.org/licensing
20df462f 21 *
7aa78908 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.
20df462f 27 * It should **NOT** be accessed directly outside of tested core methods as it
28 * may change.
812b2c0c
EM
29 *
30 * @internal
7aa78908 31 */
32class 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
4df23f2a
EM
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
ca44bb7e
EM
105 /**
106 * Financial type id to use for any lines where is is not provided.
107 *
108 * @var int
109 */
110 protected $defaultFinancialTypeID;
111
4df23f2a
EM
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 = TRUE;
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
ca44bb7e
EM
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
7aa78908 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
ca1b238b
EM
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
60e5cf34
EM
234 /**
235 * Cache of the default price field value ID.
236 *
237 * @var array
238 */
239 protected $defaultPriceFieldValueID;
240
ca1b238b
EM
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
7aa78908 289 /**
290 * Metadata for price fields.
291 *
292 * @var array
293 */
294 protected $priceFieldMetadata = [];
295
be2e79c8 296 /**
297 * Metadata for price sets.
298 *
299 * @var array
300 */
301 protected $priceSetMetadata = [];
302
20df462f 303 /**
304 * Get form object.
305 *
812b2c0c
EM
306 * @internal use in tested core code only.
307 *
20df462f 308 * @return \CRM_Core_Form|NULL
309 */
310 public function getForm(): ?CRM_Core_Form {
311 return $this->form;
312 }
313
314 /**
315 * Set form object.
316 *
812b2c0c
EM
317 * @internal use in tested core code only.
318 *
20df462f 319 * @param \CRM_Core_Form|NULL $form
320 */
321 public function setForm(?CRM_Core_Form $form): void {
322 $this->form = $form;
323 }
324
325 /**
326 * The serialize & unserialize functions are to prevent the form being serialized & stored.
327 *
328 * The form could be potentially large & circular.
329 *
330 * We simply serialize the values needed to re-serialize the form.
331 *
332 * @return array
333 */
334 public function _serialize(): array {
335 return [
336 'OverrideTotalAmount' => $this->getOverrideTotalAmount(),
337 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(),
338 'PriceSelection' => $this->getPriceSelection(),
339 ];
340 }
341
342 /**
343 * Re-instantiate the the class with non-calculated variables.
344 *
345 * @param array $data
346 */
347 public function _unserialize(array $data): void {
348 foreach ($data as $key => $value) {
349 $fn = 'set' . $key;
350 $this->$fn($value);
351 }
352 }
353
354 /**
355 * Form object - if present the buildAmount hook will be called.
356 *
357 * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal
358 */
359 protected $form;
360
7aa78908 361 /**
362 * Get Set override for total amount of the order.
363 *
812b2c0c
EM
364 * @internal use in tested core code only.
365 *
7aa78908 366 * @return float|false
367 */
368 public function getOverrideTotalAmount() {
77274636
EM
369 // The override amount is only valid for quick config price sets where more
370 // than one field has not been selected.
4df23f2a 371 if (!$this->overrideTotalAmount || $this->getLineItemCount() > 1) {
7aa78908 372 return FALSE;
373 }
77274636 374 return $this->overrideTotalAmount;
7aa78908 375 }
376
4df23f2a
EM
377 /**
378 * Is the line item financial type to be overridden.
379 *
380 * We have a tested scenario for repeatcontribution where the line item
381 * does not match the top level financial type for the order. In this case
382 * any financial type override has been determined to NOT apply to the line items.
383 *
384 * This is locked in via testRepeatTransactionUpdatedFinancialTypeAndNotEquals.
385 *
386 * @param int $financialTypeID
387 *
388 * @return bool
389 */
390 public function isOverrideLineItemFinancialType(int $financialTypeID) {
391 if (!$this->getOverrideFinancialTypeID()) {
392 return FALSE;
393 }
394 if (!$this->getOverridableFinancialTypeID()) {
395 return TRUE;
396 }
397 return $this->getOverridableFinancialTypeID() === $financialTypeID;
398 }
399
7aa78908 400 /**
401 * Set override for total amount.
402 *
812b2c0c
EM
403 * @internal use in tested core code only.
404 *
4df23f2a 405 * @param float|null $overrideTotalAmount
7aa78908 406 */
4df23f2a 407 public function setOverrideTotalAmount(?float $overrideTotalAmount): void {
77274636 408 $this->overrideTotalAmount = $overrideTotalAmount;
7aa78908 409 }
410
411 /**
412 * Get override for total amount.
413 *
812b2c0c
EM
414 * @internal use in tested core code only.
415 *
7aa78908 416 * @return int| FALSE
417 */
418 public function getOverrideFinancialTypeID() {
4df23f2a
EM
419 // We don't permit overrides if there is more than one line.
420 // The reason for this constraint may be more historical since
421 // the case could be made that if it is set it should be used and
422 // we have built out the tax calculations a lot now.
423 if (!$this->isPermitOverrideFinancialTypeForMultipleLines() && $this->getLineItemCount() > 1) {
7aa78908 424 return FALSE;
425 }
426 return $this->overrideFinancialTypeID ?? FALSE;
427 }
428
429 /**
430 * Set override for financial type ID.
431 *
812b2c0c
EM
432 * @internal use in tested core code only.
433 *
4df23f2a 434 * @param int|null $overrideFinancialTypeID
7aa78908 435 */
4df23f2a 436 public function setOverrideFinancialTypeID(?int $overrideFinancialTypeID): void {
7aa78908 437 $this->overrideFinancialTypeID = $overrideFinancialTypeID;
438 }
439
440 /**
441 * Getter for price set id.
442 *
812b2c0c
EM
443 * @internal use in tested core code only.
444 *
7aa78908 445 * @return int
812b2c0c
EM
446 *
447 * @throws \API_Exception
7aa78908 448 */
449 public function getPriceSetID(): int {
812b2c0c
EM
450 if (!$this->priceSetID) {
451 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
452 $this->setPriceSetIDFromSelectedField($fieldID);
453 }
454 }
7aa78908 455 return $this->priceSetID;
456 }
457
458 /**
459 * Setter for price set id.
460 *
812b2c0c
EM
461 * @internal use in tested core code only.
462 *
7aa78908 463 * @param int $priceSetID
464 */
465 public function setPriceSetID(int $priceSetID) {
466 $this->priceSetID = $priceSetID;
467 }
468
812b2c0c
EM
469 /**
470 * Set price set id to the default.
471 *
472 * @param string $component [membership|contribution]
473 *
474 * @throws \API_Exception
475 * @internal use in tested core code only.
476 */
477 public function setPriceSetToDefault(string $component): void {
ca1b238b 478 $this->priceSetID = $this->getDefaultPriceSetForComponent($component);
812b2c0c
EM
479 }
480
d6c86ce5
EM
481 /**
482 * Set price set ID based on the contribution page id.
483 *
812b2c0c
EM
484 * @internal use in tested core code only.
485 *
d6c86ce5
EM
486 * @param int $contributionPageID
487 *
d6c86ce5
EM
488 */
489 public function setPriceSetIDByContributionPageID(int $contributionPageID): void {
490 $this->setPriceSetIDByEntity('contribution_page', $contributionPageID);
491 }
492
493 /**
494 * Set price set ID based on the event id.
495 *
812b2c0c
EM
496 * @internal use in tested core code only.
497 *
d6c86ce5
EM
498 * @param int $eventID
499 *
500 * @throws \CiviCRM_API3_Exception
501 */
502 public function setPriceSetIDByEventPageID(int $eventID): void {
503 $this->setPriceSetIDByEntity('event', $eventID);
504 }
505
506 /**
507 * Set the price set id based on looking up the entity.
812b2c0c
EM
508 *
509 * @internal use in tested core code only.
510 *
d6c86ce5
EM
511 * @param string $entity
512 * @param int $id
513 *
514 */
515 protected function setPriceSetIDByEntity(string $entity, int $id): void {
516 $this->priceSetID = CRM_Price_BAO_PriceSet::getFor('civicrm_' . $entity, $id);
517 }
518
7aa78908 519 /**
520 * Getter for price selection.
521 *
812b2c0c
EM
522 * @internal use in tested core code only.
523 *
7aa78908 524 * @return array
525 */
526 public function getPriceSelection(): array {
527 return $this->priceSelection;
528 }
529
530 /**
531 * Setter for price selection.
532 *
812b2c0c
EM
533 * @internal use in tested core code only.
534 *
7aa78908 535 * @param array $priceSelection
536 */
537 public function setPriceSelection(array $priceSelection) {
538 $this->priceSelection = $priceSelection;
539 }
540
541 /**
542 * Price options the simplified price fields selections.
543 *
544 * ie. the 'price_' is stripped off the key name and the field ID
545 * is cast to an integer.
546 *
812b2c0c
EM
547 * @internal use in tested core code only.
548 *
7aa78908 549 * @return array
550 */
551 public function getPriceOptions():array {
552 $priceOptions = [];
553 foreach ($this->getPriceSelection() as $fieldName => $value) {
554 $fieldID = substr($fieldName, 6);
555 $priceOptions[(int) $fieldID] = $value;
556 }
557 return $priceOptions;
558 }
559
560 /**
561 * Get the metadata for the given field.
562 *
812b2c0c
EM
563 * @internal use in tested core code only.
564 *
7aa78908 565 * @param int $id
566 *
567 * @return array
7aa78908 568 */
569 public function getPriceFieldSpec(int $id) :array {
de668aa8 570 return $this->getPriceFieldsMetadata()[$id];
571 }
572
573 /**
574 * Get the metadata for the fields in the price set.
575 *
812b2c0c
EM
576 * @internal use in tested core code only.
577 *
de668aa8 578 * @return array
579 */
580 public function getPriceFieldsMetadata(): array {
581 if (empty($this->priceFieldMetadata)) {
be2e79c8 582 $this->getPriceSetMetadata();
7aa78908 583 }
de668aa8 584 return $this->priceFieldMetadata;
7aa78908 585 }
586
812b2c0c
EM
587 /**
588 * Set the metadata for the order.
589 *
590 * @param array $metadata
591 */
592 protected function setPriceFieldMetadata($metadata) {
593 $this->priceFieldMetadata = $metadata;
594 if ($this->getForm()) {
595 CRM_Utils_Hook::buildAmount($this->form->getFormContext(), $this->form, $this->priceFieldMetadata);
596 }
597 }
598
be2e79c8 599 /**
600 * Get the metadata for the fields in the price set.
601 *
812b2c0c
EM
602 * @internal use in tested core code only.
603 *
be2e79c8 604 * @return array
605 */
606 public function getPriceSetMetadata(): array {
607 if (empty($this->priceSetMetadata)) {
608 $priceSetMetadata = CRM_Price_BAO_PriceSet::getCachedPriceSetDetail($this->getPriceSetID());
812b2c0c 609 $this->setPriceFieldMetadata($priceSetMetadata['fields']);
be2e79c8 610 unset($priceSetMetadata['fields']);
611 $this->priceSetMetadata = $priceSetMetadata;
612 }
613 return $this->priceSetMetadata;
614 }
615
616 /**
617 * Get the financial type id for the order.
618 *
812b2c0c
EM
619 * @internal use in tested core code only.
620 *
be2e79c8 621 * This may differ to the line items....
622 *
623 * @return int
624 */
625 public function getFinancialTypeID(): int {
626 return (int) $this->getOverrideFinancialTypeID() ?: $this->getPriceSetMetadata()['financial_type_id'];
627 }
628
7aa78908 629 /**
812b2c0c
EM
630 * Set the price field selection from an array of params containing price
631 * fields.
7aa78908 632 *
812b2c0c
EM
633 * This function takes the sort of 'anything & everything' parameters that
634 * come in from the form layer and filters them before assigning them to the
635 * priceSelection property.
7aa78908 636 *
637 * @param array $input
812b2c0c
EM
638 *
639 * @throws \API_Exception
7aa78908 640 */
de668aa8 641 public function setPriceSelectionFromUnfilteredInput(array $input): void {
7aa78908 642 foreach ($input as $fieldName => $value) {
643 if (strpos($fieldName, 'price_') === 0) {
644 $fieldID = substr($fieldName, 6);
645 if (is_numeric($fieldID)) {
646 $this->priceSelection[$fieldName] = $value;
647 }
648 }
649 }
812b2c0c
EM
650 if (empty($this->priceSelection) && isset($input['total_amount'])
651 && is_numeric($input['total_amount']) && !empty($input['financial_type_id'])) {
ca1b238b 652 $this->priceSelection['price_' . $this->getDefaultPriceFieldID()] = $input['total_amount'];
812b2c0c
EM
653 $this->setOverrideFinancialTypeID($input['financial_type_id']);
654 }
655 }
656
657 /**
658 * Get the id of the price field to use when just an amount is provided.
659 *
660 * @throws \API_Exception
ca1b238b
EM
661 *
662 * @return int
812b2c0c 663 */
ca1b238b
EM
664 public function getDefaultPriceFieldID():int {
665 if (!$this->defaultPriceField) {
666 $this->defaultPriceField = PriceField::get(FALSE)
667 ->addWhere('name', '=', 'contribution_amount')
668 ->addWhere('price_set_id.name', '=', 'default_contribution_amount')
669 ->execute()->first();
670 }
671 return $this->defaultPriceField['id'];
7aa78908 672 }
673
60e5cf34
EM
674 /**
675 * Get the id of the price field to use when just an amount is provided.
676 *
677 * @throws \API_Exception
678 *
679 * @return int
680 */
681 public function getDefaultPriceFieldValueID():int {
682 if (!$this->defaultPriceFieldValueID) {
683 $this->defaultPriceFieldValueID = PriceFieldValue::get(FALSE)
684 ->addWhere('name', '=', 'contribution_amount')
685 ->addWhere('price_field_id.name', '=', 'contribution_amount')
686 ->execute()->first()['id'];
687 }
688 return $this->defaultPriceFieldValueID;
689 }
690
7aa78908 691 /**
692 * Get line items.
693 *
694 * return array
695 *
696 * @throws \CiviCRM_API3_Exception
697 */
698 public function getLineItems():array {
699 if (empty($this->lineItems)) {
700 $this->lineItems = $this->calculateLineItems();
701 }
702 return $this->lineItems;
703 }
704
77274636
EM
705 /**
706 * Get line items in a 'traditional' indexing format.
707 *
708 * This ensures the line items are indexed by
709 * price field id - as required by the contribution BAO.
710 *
711 * @throws \CiviCRM_API3_Exception
712 */
713 public function getPriceFieldIndexedLineItems(): array {
714 $lines = [];
715 foreach ($this->getLineItems() as $item) {
716 $lines[$item['price_field_id']] = $item;
717 }
718 return $lines;
719 }
720
e84c5dc2 721 /**
722 * Get line items that specifically relate to memberships.
723 *
724 * return array
725 *
726 * @throws \CiviCRM_API3_Exception
727 */
728 public function getMembershipLineItems():array {
729 $lines = $this->getLineItems();
730 foreach ($lines as $index => $line) {
731 if (empty($line['membership_type_id'])) {
732 unset($lines[$index]);
733 continue;
734 }
735 if (empty($line['membership_num_terms'])) {
736 $lines[$index]['membership_num_terms'] = 1;
737 }
738 }
739 return $lines;
740 }
741
aba748e5 742 /**
743 * Get an array of all membership types included in the order.
744 *
745 * @return array
746 *
747 * @throws \CiviCRM_API3_Exception
748 */
749 public function getMembershipTypes(): array {
750 $types = [];
751 foreach ($this->getMembershipLineItems() as $line) {
752 $types[$line['membership_type_id']] = CRM_Member_BAO_MembershipType::getMembershipType((int) $line['membership_type_id']);
753 }
754 return $types;
755 }
756
757 /**
758 * Get an array of all membership types included in the order.
759 *
760 * @return array
761 *
762 * @throws \CiviCRM_API3_Exception
763 */
764 public function getRenewableMembershipTypes(): array {
765 $types = [];
766 foreach ($this->getMembershipTypes() as $id => $type) {
767 if (!empty($type['auto_renew'])) {
768 $types[$id] = $type;
769 }
770 }
771 return $types;
772 }
773
7aa78908 774 /**
775 * @return array
4df23f2a
EM
776 *
777 * @throws \API_Exception
7aa78908 778 */
779 protected function calculateLineItems(): array {
780 $lineItems = [];
781 $params = $this->getPriceSelection();
782 if ($this->getOverrideTotalAmount() !== FALSE) {
783 // We need to do this to keep getLine from doing weird stuff but the goal
784 // is to ditch getLine next round of refactoring
785 // and make the code more sane.
786 $params['total_amount'] = $this->getOverrideTotalAmount();
787 }
788
812b2c0c
EM
789 // Dummy value to prevent e-notice in getLine. We calculate tax in this class.
790 $params['financial_type_id'] = 0;
4df23f2a
EM
791 if ($this->getTemplateContributionID()) {
792 $lineItems = $this->getLinesFromTemplateContribution();
793 }
794 else {
795 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
796 $this->setPriceSetIDFromSelectedField($fieldID);
797 $throwAwayArray = [];
798 // @todo - still using getLine for now but better to bring it to this class & do a better job.
799 $newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
800 foreach ($newLines as $newLine) {
801 $lineItems[$newLine['price_field_value_id']] = $newLine;
802 }
58315149 803 }
7aa78908 804 }
4df23f2a
EM
805 // Set the line item count here because it is needed to determine whether
806 // we can use overrides and would not be set yet if we have loaded them from
807 // a template contribution.
808 $this->setLineItemCount(count($lineItems));
7aa78908 809
7aa78908 810 foreach ($lineItems as &$lineItem) {
4df23f2a
EM
811 // Set the price set id if not set above. Note that the above
812 // requires it for line retrieval but we want to fix that as it
813 // should not be required at that point.
814 $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
7aa78908 815 // Set any pre-calculation to zero as we will calculate.
816 $lineItem['tax_amount'] = 0;
4df23f2a 817 if ($this->isOverrideLineItemFinancialType($lineItem['financial_type_id']) !== FALSE) {
7aa78908 818 $lineItem['financial_type_id'] = $this->getOverrideFinancialTypeID();
819 }
dd118b15 820 $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
7aa78908 821 if ($this->getOverrideTotalAmount() !== FALSE) {
77274636 822 $this->addTotalsToLineBasedOnOverrideTotal((int) $lineItem['financial_type_id'], $lineItem);
7aa78908 823 }
824 elseif ($taxRate) {
825 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
826 }
827 }
828 return $lineItems;
829 }
830
831 /**
58315149 832 * Get the total amount for the order.
7aa78908 833 *
834 * @return float
835 *
836 * @throws \CiviCRM_API3_Exception
837 */
838 public function getTotalTaxAmount() :float {
839 $amount = 0.0;
840 foreach ($this->getLineItems() as $lineItem) {
841 $amount += $lineItem['tax_amount'] ?? 0.0;
842 }
843 return $amount;
844 }
845
58315149 846 /**
765d02a3 847 * Get the total amount for the order.
58315149 848 *
849 * @return float
850 *
851 * @throws \CiviCRM_API3_Exception
852 */
853 public function getTotalAmount() :float {
854 $amount = 0.0;
855 foreach ($this->getLineItems() as $lineItem) {
53c8b1be 856 $amount += ($lineItem['line_total'] ?? 0.0) + ($lineItem['tax_amount'] ?? 0.0);
58315149 857 }
858 return $amount;
859 }
860
765d02a3
EM
861 /**
862 * Get the total amount relating to memberships for the order.
863 *
864 * @return float
865 *
866 * @throws \CiviCRM_API3_Exception
867 */
868 public function getMembershipTotalAmount() :float {
869 $amount = 0.0;
870 foreach ($this->getMembershipLineItems() as $lineItem) {
871 $amount += ($lineItem['line_total'] ?? 0.0) + ($lineItem['tax_amount'] ?? 0.0);
872 }
873 return $amount;
874 }
875
dd118b15 876 /**
877 * Get the tax rate for the given financial type.
878 *
879 * @param int $financialTypeID
880 *
881 * @return float
882 */
883 public function getTaxRate(int $financialTypeID) {
884 $taxRates = CRM_Core_PseudoConstant::getTaxRates();
885 if (!isset($taxRates[$financialTypeID])) {
886 return 0;
887 }
888 return $taxRates[$financialTypeID];
889 }
890
812b2c0c
EM
891 /**
892 * @param $fieldID
893 *
894 * @throws \API_Exception
895 */
896 protected function setPriceSetIDFromSelectedField($fieldID): void {
897 if (!isset($this->priceSetID)) {
898 $this->setPriceSetID(PriceField::get(FALSE)
899 ->addSelect('price_set_id')
900 ->addWhere('id', '=', $fieldID)
901 ->execute()
902 ->first()['price_set_id']);
903 }
904 }
905
ca44bb7e
EM
906 /**
907 * Set the line item.
908 *
909 * This function augments the line item where possible. The calling code
910 * should not attempt to set taxes. This function allows minimal values
911 * to be passed for the default price sets - ie if only membership_type_id is
912 * specified the price_field_id and price_value_id will be determined.
913 *
914 * @param array $lineItem
915 * @param int|string $index
916 *
917 * @throws \API_Exception
918 * @internal tested core code usage only.
919 * @internal use in tested core code only.
920 *
921 */
922 public function setLineItem(array $lineItem, $index): void {
77274636
EM
923 if (!isset($this->priceSetID)) {
924 if (!empty($lineItem['price_field_id'])) {
925 $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
926 }
927 else {
928 // we are using either the default membership or default contribution
929 // If membership type is passed in we use the default price field.
930 $component = !empty($lineItem['membership_type_id']) ? 'membership' : 'contribution';
931 $this->setPriceSetToDefault($component);
932 }
ca44bb7e
EM
933 }
934 if (!isset($lineItem['financial_type_id'])) {
935 $lineItem['financial_type_id'] = $this->getDefaultFinancialTypeID();
936 }
937 if (!is_numeric($lineItem['financial_type_id'])) {
938 $lineItem['financial_type_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', $lineItem['financial_type_id']);
939 }
77274636
EM
940 if ($this->getOverrideTotalAmount()) {
941 $this->addTotalsToLineBasedOnOverrideTotal((int) $lineItem['financial_type_id'], $lineItem);
942 }
943 else {
944 $lineItem['tax_amount'] = ($this->getTaxRate($lineItem['financial_type_id']) / 100) * $lineItem['line_total'];
945 }
ca44bb7e
EM
946 if (!empty($lineItem['membership_type_id'])) {
947 $lineItem['entity_table'] = 'civicrm_membership';
948 if (empty($lineItem['price_field_id']) && empty($lineItem['price_field_value_id'])) {
ca44bb7e
EM
949 $lineItem = $this->fillMembershipLine($lineItem);
950 }
951 }
ca1b238b
EM
952 if ($this->getPriceSetID() === $this->getDefaultPriceSetForComponent('contribution')) {
953 $this->fillDefaultContributionLine($lineItem);
954 }
ca44bb7e
EM
955 $this->lineItems[$index] = $lineItem;
956 }
957
958 /**
959 * Set a value on a line item.
960 *
961 * @internal only use in core tested code.
962 *
963 * @param string $name
964 * @param mixed $value
965 * @param string|int $index
966 */
967 public function setLineItemValue(string $name, $value, $index): void {
968 $this->lineItems[$index][$name] = $value;
969 }
970
971 /**
972 * @param int|string $index
973 *
974 * @return string
975 */
976 public function getLineItemEntity($index):string {
977 // @todo - ensure entity_table is set in setLineItem, go back to enotices here.
978 return str_replace('civicrm_', '', ($this->lineItems[$index]['entity_table'] ?? 'contribution'));
979 }
980
981 /**
982 * Get the ordered line item.
983 *
984 * @param string|int $index
985 *
986 * @return array
987 */
988 public function getLineItem($index): array {
989 return $this->lineItems[$index];
990 }
991
992 /**
993 * Fills in additional data for the membership line.
994 *
995 * The minimum requirement is the membership_type_id and that priceSetID is set.
996 *
997 * @param array $lineItem
998 *
999 * @return array
1000 */
1001 protected function fillMembershipLine(array $lineItem): array {
1002 $fields = $this->getPriceFieldsMetadata();
77274636
EM
1003 foreach ($fields as $field) {
1004 if (!isset($lineItem['price_field_value_id'])) {
1005 foreach ($field['options'] as $option) {
1006 if ((int) $option['membership_type_id'] === (int) $lineItem['membership_type_id']) {
1007 $lineItem['price_field_id'] = $field['id'];
1008 $lineItem['price_field_value_id'] = $option['id'];
1009 $lineItem['qty'] = 1;
1010 }
ca44bb7e
EM
1011 }
1012 }
77274636
EM
1013 if (isset($lineItem['price_field_value_id'], $field['options'][$lineItem['price_field_value_id']])) {
1014 $option = $field['options'][$lineItem['price_field_value_id']];
1015 }
ca44bb7e 1016 }
ca44bb7e
EM
1017 $lineItem['unit_price'] = $lineItem['line_total'] ?? $option['amount'];
1018 $lineItem['label'] = $lineItem['label'] ?? $option['label'];
1019 $lineItem['field_title'] = $lineItem['field_title'] ?? $option['label'];
1020 $lineItem['financial_type_id'] = $lineItem['financial_type_id'] ?: ($this->getDefaultFinancialTypeID() ?? $option['financial_type_id']);
1021 return $lineItem;
1022 }
1023
77274636
EM
1024 /**
1025 * Add total_amount and tax_amount to the line from the override total.
1026 *
1027 * @param int $financialTypeID
1028 * @param array $lineItem
1029 *
1030 * @return void
1031 */
1032 protected function addTotalsToLineBasedOnOverrideTotal(int $financialTypeID, array &$lineItem): void {
1033 $taxRate = $this->getTaxRate($financialTypeID);
1034 if ($taxRate) {
1035 // Total is tax inclusive.
1036 $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 + ($taxRate / 100));
1037 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount() - $lineItem['tax_amount'];
1038 }
1039 else {
1040 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount();
1041 }
1042 }
1043
4df23f2a
EM
1044 /**
1045 * Get the line items from a template.
1046 *
1047 * @return \Civi\Api4\Generic\Result
1048 *
1049 * @throws \API_Exception
1050 */
1051 protected function getLinesFromTemplateContribution(): array {
1052 $lines = $this->getLinesForContribution();
1053 foreach ($lines as &$line) {
1054 // The apiv4 insists on adding id - so let it get all the details
1055 // and we will filter out those that are not part of a template here.
1056 unset($line['id'], $line['contribution_id']);
1057 }
1058 return $lines;
1059 }
1060
502822e7
EM
1061 /**
1062 * Get the constructed line items formatted for the v3 Order api.
1063 *
1064 * @return array
1065 *
1066 * @internal core tested code only.
1067 *
1068 * @throws \CiviCRM_API3_Exception
1069 */
1070 public function getLineItemForV3OrderApi(): array {
1071 $lineItems = [];
1072 foreach ($this->getLineItems() as $key => $line) {
1073 $lineItems[] = [
1074 'line_item' => [$line['price_field_value_id'] => $line],
1075 'params' => $this->entityParameters[$key] ?? [],
1076 ];
1077 }
1078 return $lineItems;
1079 }
1080
4df23f2a
EM
1081 /**
1082 * @return array
1083 * @throws \API_Exception
1084 * @throws \Civi\API\Exception\UnauthorizedException
1085 */
1086 protected function getLinesForContribution(): array {
1087 return (array) LineItem::get(FALSE)
1088 ->addWhere('contribution_id', '=', $this->getTemplateContributionID())
1089 ->setSelect([
1090 'contribution_id',
1091 'entity_id',
1092 'entity_table',
1093 'price_field_id',
1094 'price_field_value_id',
1095 'financial_type_id',
1096 'label',
1097 'qty',
1098 'unit_price',
1099 'line_total',
1100 'tax_amount',
1101 'non_deductible_amount',
1102 'participant_count',
1103 'membership_num_terms',
1104 ])
1105 ->execute();
1106 }
1107
ca1b238b
EM
1108 /**
1109 * Get the default price set id for the given component.
1110 *
1111 * @param string $component
1112 *
1113 * @return int
1114 * @throws \API_Exception
1115 */
1116 protected function getDefaultPriceSetForComponent(string $component): int {
1117 if (!isset($this->defaultPriceSets[$component])) {
1118 $this->defaultPriceSets[$component] = PriceSet::get(FALSE)
1119 ->addWhere('name', '=', ($component === 'membership' ? 'default_membership_type_amount' : 'default_contribution_amount'))
1120 ->execute()
1121 ->first()['id'];
1122 }
1123 return $this->defaultPriceSets[$component];
1124 }
1125
1126 /**
1127 * Fill in values for a default contribution line item.
1128 *
1129 * @param array $lineItem
1130 *
1131 * @throws \API_Exception
1132 */
1133 protected function fillDefaultContributionLine(array &$lineItem): void {
1134 $defaults = [
1135 'qty' => 1,
1136 'price_field_id' => $this->getDefaultPriceFieldID(),
60e5cf34 1137 'price_field_value_id' => $this->getDefaultPriceFieldValueID(),
ca1b238b
EM
1138 'entity_table' => 'civicrm_contribution',
1139 'unit_price' => $lineItem['line_total'],
1140 'label' => ts('Contribution Amount'),
1141 ];
1142 $lineItem = array_merge($defaults, $lineItem);
1143 }
1144
7aa78908 1145}