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 | |
20df462f |
81 | /** |
82 | * Get form object. |
83 | * |
84 | * @return \CRM_Core_Form|NULL |
85 | */ |
86 | public function getForm(): ?CRM_Core_Form { |
87 | return $this->form; |
88 | } |
89 | |
90 | /** |
91 | * Set form object. |
92 | * |
93 | * @param \CRM_Core_Form|NULL $form |
94 | */ |
95 | public function setForm(?CRM_Core_Form $form): void { |
96 | $this->form = $form; |
97 | } |
98 | |
99 | /** |
100 | * The serialize & unserialize functions are to prevent the form being serialized & stored. |
101 | * |
102 | * The form could be potentially large & circular. |
103 | * |
104 | * We simply serialize the values needed to re-serialize the form. |
105 | * |
106 | * @return array |
107 | */ |
108 | public function _serialize(): array { |
109 | return [ |
110 | 'OverrideTotalAmount' => $this->getOverrideTotalAmount(), |
111 | 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(), |
112 | 'PriceSelection' => $this->getPriceSelection(), |
113 | ]; |
114 | } |
115 | |
116 | /** |
117 | * Re-instantiate the the class with non-calculated variables. |
118 | * |
119 | * @param array $data |
120 | */ |
121 | public function _unserialize(array $data): void { |
122 | foreach ($data as $key => $value) { |
123 | $fn = 'set' . $key; |
124 | $this->$fn($value); |
125 | } |
126 | } |
127 | |
128 | /** |
129 | * Form object - if present the buildAmount hook will be called. |
130 | * |
131 | * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal |
132 | */ |
133 | protected $form; |
134 | |
7aa78908 |
135 | /** |
136 | * Get Set override for total amount of the order. |
137 | * |
138 | * @return float|false |
139 | */ |
140 | public function getOverrideTotalAmount() { |
141 | if (count($this->getPriceOptions()) !== 1) { |
142 | return FALSE; |
143 | } |
144 | return $this->overrideTotalAmount ?? FALSE; |
145 | } |
146 | |
147 | /** |
148 | * Set override for total amount. |
149 | * |
150 | * @param float $overrideTotalAmount |
151 | */ |
20df462f |
152 | public function setOverrideTotalAmount(float $overrideTotalAmount): void { |
7aa78908 |
153 | $this->overrideTotalAmount = $overrideTotalAmount; |
154 | } |
155 | |
156 | /** |
157 | * Get override for total amount. |
158 | * |
159 | * @return int| FALSE |
160 | */ |
161 | public function getOverrideFinancialTypeID() { |
162 | if (count($this->getPriceOptions()) !== 1) { |
163 | return FALSE; |
164 | } |
165 | return $this->overrideFinancialTypeID ?? FALSE; |
166 | } |
167 | |
168 | /** |
169 | * Set override for financial type ID. |
170 | * |
171 | * @param int $overrideFinancialTypeID |
172 | */ |
173 | public function setOverrideFinancialTypeID(int $overrideFinancialTypeID) { |
174 | $this->overrideFinancialTypeID = $overrideFinancialTypeID; |
175 | } |
176 | |
177 | /** |
178 | * Getter for price set id. |
179 | * |
180 | * @return int |
181 | */ |
182 | public function getPriceSetID(): int { |
183 | return $this->priceSetID; |
184 | } |
185 | |
186 | /** |
187 | * Setter for price set id. |
188 | * |
189 | * @param int $priceSetID |
190 | */ |
191 | public function setPriceSetID(int $priceSetID) { |
192 | $this->priceSetID = $priceSetID; |
193 | } |
194 | |
195 | /** |
196 | * Getter for price selection. |
197 | * |
198 | * @return array |
199 | */ |
200 | public function getPriceSelection(): array { |
201 | return $this->priceSelection; |
202 | } |
203 | |
204 | /** |
205 | * Setter for price selection. |
206 | * |
207 | * @param array $priceSelection |
208 | */ |
209 | public function setPriceSelection(array $priceSelection) { |
210 | $this->priceSelection = $priceSelection; |
211 | } |
212 | |
213 | /** |
214 | * Price options the simplified price fields selections. |
215 | * |
216 | * ie. the 'price_' is stripped off the key name and the field ID |
217 | * is cast to an integer. |
218 | * |
219 | * @return array |
220 | */ |
221 | public function getPriceOptions():array { |
222 | $priceOptions = []; |
223 | foreach ($this->getPriceSelection() as $fieldName => $value) { |
224 | $fieldID = substr($fieldName, 6); |
225 | $priceOptions[(int) $fieldID] = $value; |
226 | } |
227 | return $priceOptions; |
228 | } |
229 | |
230 | /** |
231 | * Get the metadata for the given field. |
232 | * |
233 | * @param int $id |
234 | * |
235 | * @return array |
7aa78908 |
236 | */ |
237 | public function getPriceFieldSpec(int $id) :array { |
de668aa8 |
238 | return $this->getPriceFieldsMetadata()[$id]; |
239 | } |
240 | |
241 | /** |
242 | * Get the metadata for the fields in the price set. |
243 | * |
244 | * @return array |
245 | */ |
246 | public function getPriceFieldsMetadata(): array { |
247 | if (empty($this->priceFieldMetadata)) { |
7aa78908 |
248 | $this->priceFieldMetadata = CRM_Price_BAO_PriceSet::getCachedPriceSetDetail($this->getPriceSetID())['fields']; |
20df462f |
249 | if ($this->getForm()) { |
250 | CRM_Utils_Hook::buildAmount($this->form->getFormContext(), $this->form, $this->priceFieldMetadata); |
251 | } |
7aa78908 |
252 | } |
de668aa8 |
253 | return $this->priceFieldMetadata; |
7aa78908 |
254 | } |
255 | |
256 | /** |
257 | * Set the price field selection from an array of params containing price fields. |
258 | * |
259 | * This function takes the sort of 'anything & everything' parameters that come in from the |
260 | * form layer and filters them before assigning them to the priceSelection property. |
261 | * |
262 | * @param array $input |
263 | */ |
de668aa8 |
264 | public function setPriceSelectionFromUnfilteredInput(array $input): void { |
7aa78908 |
265 | foreach ($input as $fieldName => $value) { |
266 | if (strpos($fieldName, 'price_') === 0) { |
267 | $fieldID = substr($fieldName, 6); |
268 | if (is_numeric($fieldID)) { |
269 | $this->priceSelection[$fieldName] = $value; |
270 | } |
271 | } |
272 | } |
273 | } |
274 | |
275 | /** |
276 | * Get line items. |
277 | * |
278 | * return array |
279 | * |
280 | * @throws \CiviCRM_API3_Exception |
281 | */ |
282 | public function getLineItems():array { |
283 | if (empty($this->lineItems)) { |
284 | $this->lineItems = $this->calculateLineItems(); |
285 | } |
286 | return $this->lineItems; |
287 | } |
288 | |
289 | /** |
290 | * @return array |
291 | * @throws \CiviCRM_API3_Exception |
292 | */ |
293 | protected function calculateLineItems(): array { |
294 | $lineItems = []; |
295 | $params = $this->getPriceSelection(); |
296 | if ($this->getOverrideTotalAmount() !== FALSE) { |
297 | // We need to do this to keep getLine from doing weird stuff but the goal |
298 | // is to ditch getLine next round of refactoring |
299 | // and make the code more sane. |
300 | $params['total_amount'] = $this->getOverrideTotalAmount(); |
301 | } |
302 | |
303 | foreach ($this->getPriceOptions() as $fieldID => $valueID) { |
58315149 |
304 | if (!isset($this->priceSetID)) { |
305 | $this->setPriceSetID(PriceField::get()->addSelect('price_set_id')->addWhere('id', '=', $fieldID)->execute()->first()['price_set_id']); |
306 | } |
7aa78908 |
307 | $throwAwayArray = []; |
308 | // @todo - still using getLine for now but better to bring it to this class & do a better job. |
737bd87c |
309 | $newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1]; |
58315149 |
310 | foreach ($newLines as $newLine) { |
311 | $lineItems[$newLine['price_field_value_id']] = $newLine; |
312 | } |
7aa78908 |
313 | } |
314 | |
7aa78908 |
315 | foreach ($lineItems as &$lineItem) { |
316 | // Set any pre-calculation to zero as we will calculate. |
317 | $lineItem['tax_amount'] = 0; |
318 | if ($this->getOverrideFinancialTypeID() !== FALSE) { |
319 | $lineItem['financial_type_id'] = $this->getOverrideFinancialTypeID(); |
320 | } |
dd118b15 |
321 | $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']); |
7aa78908 |
322 | if ($this->getOverrideTotalAmount() !== FALSE) { |
323 | if ($taxRate) { |
324 | // Total is tax inclusive. |
dd118b15 |
325 | $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 + ($taxRate / 100)); |
7aa78908 |
326 | $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount() - $lineItem['tax_amount']; |
327 | } |
328 | else { |
329 | $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount(); |
330 | } |
331 | } |
332 | elseif ($taxRate) { |
333 | $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total']; |
334 | } |
335 | } |
336 | return $lineItems; |
337 | } |
338 | |
339 | /** |
58315149 |
340 | * Get the total amount for the order. |
7aa78908 |
341 | * |
342 | * @return float |
343 | * |
344 | * @throws \CiviCRM_API3_Exception |
345 | */ |
346 | public function getTotalTaxAmount() :float { |
347 | $amount = 0.0; |
348 | foreach ($this->getLineItems() as $lineItem) { |
349 | $amount += $lineItem['tax_amount'] ?? 0.0; |
350 | } |
351 | return $amount; |
352 | } |
353 | |
58315149 |
354 | /** |
355 | * Get the total tax amount for the order. |
356 | * |
357 | * @return float |
358 | * |
359 | * @throws \CiviCRM_API3_Exception |
360 | */ |
361 | public function getTotalAmount() :float { |
362 | $amount = 0.0; |
363 | foreach ($this->getLineItems() as $lineItem) { |
364 | $amount += $lineItem['line_total'] ?? 0.0; |
365 | } |
366 | return $amount; |
367 | } |
368 | |
dd118b15 |
369 | /** |
370 | * Get the tax rate for the given financial type. |
371 | * |
372 | * @param int $financialTypeID |
373 | * |
374 | * @return float |
375 | */ |
376 | public function getTaxRate(int $financialTypeID) { |
377 | $taxRates = CRM_Core_PseudoConstant::getTaxRates(); |
378 | if (!isset($taxRates[$financialTypeID])) { |
379 | return 0; |
380 | } |
381 | return $taxRates[$financialTypeID]; |
382 | } |
383 | |
7aa78908 |
384 | } |