Merge pull request #19884 from eileenmcnaughton/unit8less
[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\PriceField;
13
14 /**
15 *
16 * @package CRM
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
18 *
19 * Order class.
20 *
21 * This class is intended to become the object to manage orders, including via Order.api.
22 *
23 * As of writing it is in the process of having appropriate functions built up.
24 * It should **NOT** be accessed directly outside of tested core methods as it
25 * may change.
26 */
27 class CRM_Financial_BAO_Order {
28
29 /**
30 * Price set id.
31 *
32 * @var int
33 */
34 protected $priceSetID;
35
36 /**
37 * Selected price items in the format we see in forms.
38 *
39 * ie.
40 * [price_3 => 4, price_10 => 7]
41 * is equivalent to 'option_value 4 for radio price field 3 and
42 * a quantity of 7 for text price field 10.
43 *
44 * @var array
45 */
46 protected $priceSelection = [];
47
48 /**
49 * Override for financial type id.
50 *
51 * Used when the financial type id is to be overridden for all line items
52 * (as can happen in backoffice forms)
53 *
54 * @var int
55 */
56 protected $overrideFinancialTypeID;
57
58 /**
59 * Override for the total amount of the order.
60 *
61 * When there is a single line item the order total may be overriden.
62 *
63 * @var float
64 */
65 protected $overrideTotalAmount;
66
67 /**
68 * Line items in the order.
69 *
70 * @var array
71 */
72 protected $lineItems = [];
73
74 /**
75 * Metadata for price fields.
76 *
77 * @var array
78 */
79 protected $priceFieldMetadata = [];
80
81 /**
82 * Metadata for price sets.
83 *
84 * @var array
85 */
86 protected $priceSetMetadata = [];
87
88 /**
89 * Get form object.
90 *
91 * @return \CRM_Core_Form|NULL
92 */
93 public function getForm(): ?CRM_Core_Form {
94 return $this->form;
95 }
96
97 /**
98 * Set form object.
99 *
100 * @param \CRM_Core_Form|NULL $form
101 */
102 public function setForm(?CRM_Core_Form $form): void {
103 $this->form = $form;
104 }
105
106 /**
107 * The serialize & unserialize functions are to prevent the form being serialized & stored.
108 *
109 * The form could be potentially large & circular.
110 *
111 * We simply serialize the values needed to re-serialize the form.
112 *
113 * @return array
114 */
115 public function _serialize(): array {
116 return [
117 'OverrideTotalAmount' => $this->getOverrideTotalAmount(),
118 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(),
119 'PriceSelection' => $this->getPriceSelection(),
120 ];
121 }
122
123 /**
124 * Re-instantiate the the class with non-calculated variables.
125 *
126 * @param array $data
127 */
128 public function _unserialize(array $data): void {
129 foreach ($data as $key => $value) {
130 $fn = 'set' . $key;
131 $this->$fn($value);
132 }
133 }
134
135 /**
136 * Form object - if present the buildAmount hook will be called.
137 *
138 * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal
139 */
140 protected $form;
141
142 /**
143 * Get Set override for total amount of the order.
144 *
145 * @return float|false
146 */
147 public function getOverrideTotalAmount() {
148 if (count($this->getPriceOptions()) !== 1) {
149 return FALSE;
150 }
151 return $this->overrideTotalAmount ?? FALSE;
152 }
153
154 /**
155 * Set override for total amount.
156 *
157 * @param float $overrideTotalAmount
158 */
159 public function setOverrideTotalAmount(float $overrideTotalAmount): void {
160 $this->overrideTotalAmount = $overrideTotalAmount;
161 }
162
163 /**
164 * Get override for total amount.
165 *
166 * @return int| FALSE
167 */
168 public function getOverrideFinancialTypeID() {
169 if (count($this->getPriceOptions()) !== 1) {
170 return FALSE;
171 }
172 return $this->overrideFinancialTypeID ?? FALSE;
173 }
174
175 /**
176 * Set override for financial type ID.
177 *
178 * @param int $overrideFinancialTypeID
179 */
180 public function setOverrideFinancialTypeID(int $overrideFinancialTypeID) {
181 $this->overrideFinancialTypeID = $overrideFinancialTypeID;
182 }
183
184 /**
185 * Getter for price set id.
186 *
187 * @return int
188 */
189 public function getPriceSetID(): int {
190 return $this->priceSetID;
191 }
192
193 /**
194 * Setter for price set id.
195 *
196 * @param int $priceSetID
197 */
198 public function setPriceSetID(int $priceSetID) {
199 $this->priceSetID = $priceSetID;
200 }
201
202 /**
203 * Getter for price selection.
204 *
205 * @return array
206 */
207 public function getPriceSelection(): array {
208 return $this->priceSelection;
209 }
210
211 /**
212 * Setter for price selection.
213 *
214 * @param array $priceSelection
215 */
216 public function setPriceSelection(array $priceSelection) {
217 $this->priceSelection = $priceSelection;
218 }
219
220 /**
221 * Price options the simplified price fields selections.
222 *
223 * ie. the 'price_' is stripped off the key name and the field ID
224 * is cast to an integer.
225 *
226 * @return array
227 */
228 public function getPriceOptions():array {
229 $priceOptions = [];
230 foreach ($this->getPriceSelection() as $fieldName => $value) {
231 $fieldID = substr($fieldName, 6);
232 $priceOptions[(int) $fieldID] = $value;
233 }
234 return $priceOptions;
235 }
236
237 /**
238 * Get the metadata for the given field.
239 *
240 * @param int $id
241 *
242 * @return array
243 */
244 public function getPriceFieldSpec(int $id) :array {
245 return $this->getPriceFieldsMetadata()[$id];
246 }
247
248 /**
249 * Get the metadata for the fields in the price set.
250 *
251 * @return array
252 */
253 public function getPriceFieldsMetadata(): array {
254 if (empty($this->priceFieldMetadata)) {
255 $this->getPriceSetMetadata();
256 if ($this->getForm()) {
257 CRM_Utils_Hook::buildAmount($this->form->getFormContext(), $this->form, $this->priceFieldMetadata);
258 }
259 }
260 return $this->priceFieldMetadata;
261 }
262
263 /**
264 * Get the metadata for the fields in the price set.
265 *
266 * @return array
267 */
268 public function getPriceSetMetadata(): array {
269 if (empty($this->priceSetMetadata)) {
270 $priceSetMetadata = CRM_Price_BAO_PriceSet::getCachedPriceSetDetail($this->getPriceSetID());
271 $this->priceFieldMetadata = $priceSetMetadata['fields'];
272 unset($priceSetMetadata['fields']);
273 $this->priceSetMetadata = $priceSetMetadata;
274 }
275 return $this->priceSetMetadata;
276 }
277
278 /**
279 * Get the financial type id for the order.
280 *
281 * This may differ to the line items....
282 *
283 * @return int
284 */
285 public function getFinancialTypeID(): int {
286 return (int) $this->getOverrideFinancialTypeID() ?: $this->getPriceSetMetadata()['financial_type_id'];
287 }
288
289 /**
290 * Set the price field selection from an array of params containing price fields.
291 *
292 * This function takes the sort of 'anything & everything' parameters that come in from the
293 * form layer and filters them before assigning them to the priceSelection property.
294 *
295 * @param array $input
296 */
297 public function setPriceSelectionFromUnfilteredInput(array $input): void {
298 foreach ($input as $fieldName => $value) {
299 if (strpos($fieldName, 'price_') === 0) {
300 $fieldID = substr($fieldName, 6);
301 if (is_numeric($fieldID)) {
302 $this->priceSelection[$fieldName] = $value;
303 }
304 }
305 }
306 }
307
308 /**
309 * Get line items.
310 *
311 * return array
312 *
313 * @throws \CiviCRM_API3_Exception
314 */
315 public function getLineItems():array {
316 if (empty($this->lineItems)) {
317 $this->lineItems = $this->calculateLineItems();
318 }
319 return $this->lineItems;
320 }
321
322 /**
323 * @return array
324 * @throws \CiviCRM_API3_Exception
325 */
326 protected function calculateLineItems(): array {
327 $lineItems = [];
328 $params = $this->getPriceSelection();
329 if ($this->getOverrideTotalAmount() !== FALSE) {
330 // We need to do this to keep getLine from doing weird stuff but the goal
331 // is to ditch getLine next round of refactoring
332 // and make the code more sane.
333 $params['total_amount'] = $this->getOverrideTotalAmount();
334 }
335
336 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
337 if (!isset($this->priceSetID)) {
338 $this->setPriceSetID(PriceField::get()->addSelect('price_set_id')->addWhere('id', '=', $fieldID)->execute()->first()['price_set_id']);
339 }
340 $throwAwayArray = [];
341 // @todo - still using getLine for now but better to bring it to this class & do a better job.
342 $newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
343 foreach ($newLines as $newLine) {
344 $lineItems[$newLine['price_field_value_id']] = $newLine;
345 }
346 }
347
348 foreach ($lineItems as &$lineItem) {
349 // Set any pre-calculation to zero as we will calculate.
350 $lineItem['tax_amount'] = 0;
351 if ($this->getOverrideFinancialTypeID() !== FALSE) {
352 $lineItem['financial_type_id'] = $this->getOverrideFinancialTypeID();
353 }
354 $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
355 if ($this->getOverrideTotalAmount() !== FALSE) {
356 if ($taxRate) {
357 // Total is tax inclusive.
358 $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 + ($taxRate / 100));
359 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount() - $lineItem['tax_amount'];
360 }
361 else {
362 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount();
363 }
364 }
365 elseif ($taxRate) {
366 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
367 }
368 }
369 return $lineItems;
370 }
371
372 /**
373 * Get the total amount for the order.
374 *
375 * @return float
376 *
377 * @throws \CiviCRM_API3_Exception
378 */
379 public function getTotalTaxAmount() :float {
380 $amount = 0.0;
381 foreach ($this->getLineItems() as $lineItem) {
382 $amount += $lineItem['tax_amount'] ?? 0.0;
383 }
384 return $amount;
385 }
386
387 /**
388 * Get the total tax amount for the order.
389 *
390 * @return float
391 *
392 * @throws \CiviCRM_API3_Exception
393 */
394 public function getTotalAmount() :float {
395 $amount = 0.0;
396 foreach ($this->getLineItems() as $lineItem) {
397 $amount += $lineItem['line_total'] ?? 0.0;
398 }
399 return $amount;
400 }
401
402 /**
403 * Get the tax rate for the given financial type.
404 *
405 * @param int $financialTypeID
406 *
407 * @return float
408 */
409 public function getTaxRate(int $financialTypeID) {
410 $taxRates = CRM_Core_PseudoConstant::getTaxRates();
411 if (!isset($taxRates[$financialTypeID])) {
412 return 0;
413 }
414 return $taxRates[$financialTypeID];
415 }
416
417 }