3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
12 use Civi\Api4\PriceField
;
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
21 * This class is intended to become the object to manage orders, including via Order.api.
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
27 class CRM_Financial_BAO_Order
{
34 protected $priceSetID;
37 * Selected price items in the format we see in forms.
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.
46 protected $priceSelection = [];
49 * Override for financial type id.
51 * Used when the financial type id is to be overridden for all line items
52 * (as can happen in backoffice forms)
56 protected $overrideFinancialTypeID;
59 * Override for the total amount of the order.
61 * When there is a single line item the order total may be overriden.
65 protected $overrideTotalAmount;
68 * Line items in the order.
72 protected $lineItems = [];
75 * Metadata for price fields.
79 protected $priceFieldMetadata = [];
82 * Metadata for price sets.
86 protected $priceSetMetadata = [];
91 * @return \CRM_Core_Form|NULL
93 public function getForm(): ?CRM_Core_Form
{
100 * @param \CRM_Core_Form|NULL $form
102 public function setForm(?CRM_Core_Form
$form): void
{
107 * The serialize & unserialize functions are to prevent the form being serialized & stored.
109 * The form could be potentially large & circular.
111 * We simply serialize the values needed to re-serialize the form.
115 public function _serialize(): array {
117 'OverrideTotalAmount' => $this->getOverrideTotalAmount(),
118 'OverrideFinancialType' => $this->getOverrideFinancialTypeID(),
119 'PriceSelection' => $this->getPriceSelection(),
124 * Re-instantiate the the class with non-calculated variables.
128 public function _unserialize(array $data): void
{
129 foreach ($data as $key => $value) {
136 * Form object - if present the buildAmount hook will be called.
138 * @var \CRM_Member_Form_Membership|\CRM_Member_Form_MembershipRenewal
143 * Get Set override for total amount of the order.
145 * @return float|false
147 public function getOverrideTotalAmount() {
148 if (count($this->getPriceOptions()) !== 1) {
151 return $this->overrideTotalAmount ??
FALSE;
155 * Set override for total amount.
157 * @param float $overrideTotalAmount
159 public function setOverrideTotalAmount(float $overrideTotalAmount): void
{
160 $this->overrideTotalAmount
= $overrideTotalAmount;
164 * Get override for total amount.
168 public function getOverrideFinancialTypeID() {
169 if (count($this->getPriceOptions()) !== 1) {
172 return $this->overrideFinancialTypeID ??
FALSE;
176 * Set override for financial type ID.
178 * @param int $overrideFinancialTypeID
180 public function setOverrideFinancialTypeID(int $overrideFinancialTypeID) {
181 $this->overrideFinancialTypeID
= $overrideFinancialTypeID;
185 * Getter for price set id.
189 public function getPriceSetID(): int {
190 return $this->priceSetID
;
194 * Setter for price set id.
196 * @param int $priceSetID
198 public function setPriceSetID(int $priceSetID) {
199 $this->priceSetID
= $priceSetID;
203 * Getter for price selection.
207 public function getPriceSelection(): array {
208 return $this->priceSelection
;
212 * Setter for price selection.
214 * @param array $priceSelection
216 public function setPriceSelection(array $priceSelection) {
217 $this->priceSelection
= $priceSelection;
221 * Price options the simplified price fields selections.
223 * ie. the 'price_' is stripped off the key name and the field ID
224 * is cast to an integer.
228 public function getPriceOptions():array {
230 foreach ($this->getPriceSelection() as $fieldName => $value) {
231 $fieldID = substr($fieldName, 6);
232 $priceOptions[(int) $fieldID] = $value;
234 return $priceOptions;
238 * Get the metadata for the given field.
244 public function getPriceFieldSpec(int $id) :array {
245 return $this->getPriceFieldsMetadata()[$id];
249 * Get the metadata for the fields in the price set.
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
);
260 return $this->priceFieldMetadata
;
264 * Get the metadata for the fields in the price set.
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;
275 return $this->priceSetMetadata
;
279 * Get the financial type id for the order.
281 * This may differ to the line items....
285 public function getFinancialTypeID(): int {
286 return (int) $this->getOverrideFinancialTypeID() ?
: $this->getPriceSetMetadata()['financial_type_id'];
290 * Set the price field selection from an array of params containing price fields.
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.
295 * @param array $input
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;
313 * @throws \CiviCRM_API3_Exception
315 public function getLineItems():array {
316 if (empty($this->lineItems
)) {
317 $this->lineItems
= $this->calculateLineItems();
319 return $this->lineItems
;
324 * @throws \CiviCRM_API3_Exception
326 protected function calculateLineItems(): array {
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();
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']);
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;
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();
354 $taxRate = $this->getTaxRate((int) $lineItem['financial_type_id']);
355 if ($this->getOverrideTotalAmount() !== FALSE) {
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'];
362 $lineItem['line_total'] = $lineItem['unit_price'] = $this->getOverrideTotalAmount();
366 $lineItem['tax_amount'] = ($taxRate / 100) * $lineItem['line_total'];
373 * Get the total amount for the order.
377 * @throws \CiviCRM_API3_Exception
379 public function getTotalTaxAmount() :float {
381 foreach ($this->getLineItems() as $lineItem) {
382 $amount +
= $lineItem['tax_amount'] ??
0.0;
388 * Get the total tax amount for the order.
392 * @throws \CiviCRM_API3_Exception
394 public function getTotalAmount() :float {
396 foreach ($this->getLineItems() as $lineItem) {
397 $amount +
= $lineItem['line_total'] ??
0.0;
403 * Get the tax rate for the given financial type.
405 * @param int $financialTypeID
409 public function getTaxRate(int $financialTypeID) {
410 $taxRates = CRM_Core_PseudoConstant
::getTaxRates();
411 if (!isset($taxRates[$financialTypeID])) {
414 return $taxRates[$financialTypeID];