Merge in 5.28
[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 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 * Order class.
17 *
18 * This class is intended to become the object to manage orders, including via Order.api.
19 *
20 * As of writing it is in the process of having appropriate functions built up.
21 */
22 class CRM_Financial_BAO_Order {
23
24 /**
25 * Price set id.
26 *
27 * @var int
28 */
29 protected $priceSetID;
30
31 /**
32 * Selected price items in the format we see in forms.
33 *
34 * ie.
35 * [price_3 => 4, price_10 => 7]
36 * is equivalent to 'option_value 4 for radio price field 3 and
37 * a quantity of 7 for text price field 10.
38 *
39 * @var array
40 */
41 protected $priceSelection = [];
42
43 /**
44 * Override for financial type id.
45 *
46 * Used when the financial type id is to be overridden for all line items
47 * (as can happen in backoffice forms)
48 *
49 * @var int
50 */
51 protected $overrideFinancialTypeID;
52
53 /**
54 * Override for the total amount of the order.
55 *
56 * When there is a single line item the order total may be overriden.
57 *
58 * @var float
59 */
60 protected $overrideTotalAmount;
61
62 /**
63 * Line items in the order.
64 *
65 * @var array
66 */
67 protected $lineItems = [];
68
69 /**
70 * Metadata for price fields.
71 *
72 * @var array
73 */
74 protected $priceFieldMetadata = [];
75
76 /**
77 * Get Set override for total amount of the order.
78 *
79 * @return float|false
80 */
81 public function getOverrideTotalAmount() {
82 if (count($this->getPriceOptions()) !== 1) {
83 return FALSE;
84 }
85 return $this->overrideTotalAmount ?? FALSE;
86 }
87
88 /**
89 * Set override for total amount.
90 *
91 * @param float $overrideTotalAmount
92 */
93 public function setOverrideTotalAmount(float $overrideTotalAmount) {
94 $this->overrideTotalAmount = $overrideTotalAmount;
95 }
96
97 /**
98 * Get override for total amount.
99 *
100 * @return int| FALSE
101 */
102 public function getOverrideFinancialTypeID() {
103 if (count($this->getPriceOptions()) !== 1) {
104 return FALSE;
105 }
106 return $this->overrideFinancialTypeID ?? FALSE;
107 }
108
109 /**
110 * Set override for financial type ID.
111 *
112 * @param int $overrideFinancialTypeID
113 */
114 public function setOverrideFinancialTypeID(int $overrideFinancialTypeID) {
115 $this->overrideFinancialTypeID = $overrideFinancialTypeID;
116 }
117
118 /**
119 * Getter for price set id.
120 *
121 * @return int
122 */
123 public function getPriceSetID(): int {
124 return $this->priceSetID;
125 }
126
127 /**
128 * Setter for price set id.
129 *
130 * @param int $priceSetID
131 */
132 public function setPriceSetID(int $priceSetID) {
133 $this->priceSetID = $priceSetID;
134 }
135
136 /**
137 * Getter for price selection.
138 *
139 * @return array
140 */
141 public function getPriceSelection(): array {
142 return $this->priceSelection;
143 }
144
145 /**
146 * Setter for price selection.
147 *
148 * @param array $priceSelection
149 */
150 public function setPriceSelection(array $priceSelection) {
151 $this->priceSelection = $priceSelection;
152 }
153
154 /**
155 * Price options the simplified price fields selections.
156 *
157 * ie. the 'price_' is stripped off the key name and the field ID
158 * is cast to an integer.
159 *
160 * @return array
161 */
162 public function getPriceOptions():array {
163 $priceOptions = [];
164 foreach ($this->getPriceSelection() as $fieldName => $value) {
165 $fieldID = substr($fieldName, 6);
166 $priceOptions[(int) $fieldID] = $value;
167 }
168 return $priceOptions;
169 }
170
171 /**
172 * Get the metadata for the given field.
173 *
174 * @param int $id
175 *
176 * @return array
177 * @throws \CiviCRM_API3_Exception
178 */
179 public function getPriceFieldSpec(int $id) :array {
180 if (!isset($this->priceFieldMetadata[$id])) {
181 $this->priceFieldMetadata = CRM_Price_BAO_PriceSet::getCachedPriceSetDetail($this->getPriceSetID())['fields'];
182 }
183 return $this->priceFieldMetadata[$id];
184 }
185
186 /**
187 * Set the price field selection from an array of params containing price fields.
188 *
189 * This function takes the sort of 'anything & everything' parameters that come in from the
190 * form layer and filters them before assigning them to the priceSelection property.
191 *
192 * @param array $input
193 */
194 public function setPriceSelectionFromUnfilteredInput(array $input) {
195 foreach ($input as $fieldName => $value) {
196 if (strpos($fieldName, 'price_') === 0) {
197 $fieldID = substr($fieldName, 6);
198 if (is_numeric($fieldID)) {
199 $this->priceSelection[$fieldName] = $value;
200 }
201 }
202 }
203 }
204
205 /**
206 * Get line items.
207 *
208 * return array
209 *
210 * @throws \CiviCRM_API3_Exception
211 */
212 public function getLineItems():array {
213 if (empty($this->lineItems)) {
214 $this->lineItems = $this->calculateLineItems();
215 }
216 return $this->lineItems;
217 }
218
219 /**
220 * @return array
221 * @throws \CiviCRM_API3_Exception
222 */
223 protected function calculateLineItems(): array {
224 $lineItems = [];
225 $params = $this->getPriceSelection();
226 if ($this->getOverrideTotalAmount() !== FALSE) {
227 // We need to do this to keep getLine from doing weird stuff but the goal
228 // is to ditch getLine next round of refactoring
229 // and make the code more sane.
230 $params['total_amount'] = $this->getOverrideTotalAmount();
231 }
232
233 foreach ($this->getPriceOptions() as $fieldID => $valueID) {
234 $throwAwayArray = [];
235 // @todo - still using getLine for now but better to bring it to this class & do a better job.
236 $lineItems[$valueID] = CRM_Price_BAO_PriceSet::getLine($params, $throwAwayArray, $this->getPriceSetID(), $this->getPriceFieldSpec($fieldID), $fieldID, 0)[1][$valueID];
237 }
238
239 $taxRates = CRM_Core_PseudoConstant::getTaxRates();
240 foreach ($lineItems as &$lineItem) {
241 // Set any pre-calculation to zero as we will calculate.
242 $lineItem['tax_amount'] = 0;
243 if ($this->getOverrideFinancialTypeID() !== FALSE) {
244 $lineItem['financial_type_id'] = $this->getOverrideFinancialTypeID();
245 }
246 $taxRate = $taxRates[$lineItem['financial_type_id']] ?? 0;
247 if ($this->getOverrideTotalAmount() !== FALSE) {
248 if ($taxRate) {
249 // Total is tax inclusive.
250 $lineItem['tax_amount'] = ($taxRate / 100) * $this->getOverrideTotalAmount();
251 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount() - $lineItem['tax_amount'];
252 }
253 else {
254 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount();
255 }
256 }
257 elseif ($taxRate) {
258 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
259 }
260 }
261 return $lineItems;
262 }
263
264 /**
265 * Get the total tax amount for the order.
266 *
267 * @return float
268 *
269 * @throws \CiviCRM_API3_Exception
270 */
271 public function getTotalTaxAmount() :float {
272 $amount = 0.0;
273 foreach ($this->getLineItems() as $lineItem) {
274 $amount += $lineItem['tax_amount'] ?? 0.0;
275 }
276 return $amount;
277 }
278
279 }