Commit | Line | Data |
---|---|---|
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 | 12 | use Civi\Api4\LineItem; |
58315149 | 13 | use Civi\Api4\PriceField; |
60e5cf34 | 14 | use Civi\Api4\PriceFieldValue; |
812b2c0c | 15 | use 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 | */ |
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 | ||
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 | } |