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 | |
58315149 |
12 | use Civi\Api4\PriceField; |
13 | |
7aa78908 |
14 | /** |
15 | * |
16 | * @package CRM |
17 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
20df462f |
18 | * |
7aa78908 |
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. |
20df462f |
24 | * It should **NOT** be accessed directly outside of tested core methods as it |
25 | * may change. |
7aa78908 |
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 | |
be2e79c8 |
81 | /** |
82 | * Metadata for price sets. |
83 | * |
84 | * @var array |
85 | */ |
86 | protected $priceSetMetadata = []; |
87 | |
20df462f |
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 | |
7aa78908 |
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 | */ |
20df462f |
159 | public function setOverrideTotalAmount(float $overrideTotalAmount): void { |
7aa78908 |
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 |
7aa78908 |
243 | */ |
244 | public function getPriceFieldSpec(int $id) :array { |
de668aa8 |
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)) { |
be2e79c8 |
255 | $this->getPriceSetMetadata(); |
20df462f |
256 | if ($this->getForm()) { |
257 | CRM_Utils_Hook::buildAmount($this->form->getFormContext(), $this->form, $this->priceFieldMetadata); |
258 | } |
7aa78908 |
259 | } |
de668aa8 |
260 | return $this->priceFieldMetadata; |
7aa78908 |
261 | } |
262 | |
be2e79c8 |
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 | |
7aa78908 |
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 | */ |
de668aa8 |
297 | public function setPriceSelectionFromUnfilteredInput(array $input): void { |
7aa78908 |
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) { |
58315149 |
337 | if (!isset($this->priceSetID)) { |
338 | $this->setPriceSetID(PriceField::get()->addSelect('price_set_id')->addWhere('id', '=', $fieldID)->execute()->first()['price_set_id']); |
339 | } |
7aa78908 |
340 | $throwAwayArray = []; |
341 | // @todo - still using getLine for now but better to bring it to this class & do a better job. |
737bd87c |
342 | $newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1]; |
58315149 |
343 | foreach ($newLines as $newLine) { |
344 | $lineItems[$newLine['price_field_value_id']] = $newLine; |
345 | } |
7aa78908 |
346 | } |
347 | |
7aa78908 |
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 | } |
dd118b15 |
354 | $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']); |
7aa78908 |
355 | if ($this->getOverrideTotalAmount() !== FALSE) { |
356 | if ($taxRate) { |
357 | // Total is tax inclusive. |
dd118b15 |
358 | $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 + ($taxRate / 100)); |
7aa78908 |
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 | /** |
58315149 |
373 | * Get the total amount for the order. |
7aa78908 |
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 | |
58315149 |
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 | |
dd118b15 |
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 | |
7aa78908 |
417 | } |