Merge pull request #19580 from MegaphoneJon/core-1132
[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 * 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
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 */
152 public function setOverrideTotalAmount(float $overrideTotalAmount): void {
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
236 */
237 public function getPriceFieldSpec(int $id) :array {
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)) {
248 $this->priceFieldMetadata = CRM_Price_BAO_PriceSet::getCachedPriceSetDetail($this->getPriceSetID())['fields'];
249 if ($this->getForm()) {
250 CRM_Utils_Hook::buildAmount($this->form->getFormContext(), $this->form, $this->priceFieldMetadata);
251 }
252 }
253 return $this->priceFieldMetadata;
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 */
264 public function setPriceSelectionFromUnfilteredInput(array $input): void {
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) {
304 if (!isset($this->priceSetID)) {
305 $this->setPriceSetID(PriceField::get()->addSelect('price_set_id')->addWhere('id', '=', $fieldID)->execute()->first()['price_set_id']);
306 }
307 $throwAwayArray = [];
308 // @todo - still using getLine for now but better to bring it to this class & do a better job.
309 $newLines = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID)[1];
310 foreach ($newLines as $newLine) {
311 $lineItems[$newLine['price_field_value_id']] = $newLine;
312 }
313 }
314
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 }
321 $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
322 if ($this->getOverrideTotalAmount() !== FALSE) {
323 if ($taxRate) {
324 // Total is tax inclusive.
325 $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount() / (1 + ($taxRate / 100));
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 /**
340 * Get the total amount for the order.
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
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
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
384 }