Make Order api easier to use for default price set
authorEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 21 Jun 2021 00:36:24 +0000 (12:36 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 22 Jun 2021 06:33:15 +0000 (18:33 +1200)
This changes the order api so that it is not necessary to figure out the details
of the default price set when using it to create memberships.

CRM/Contribute/BAO/Contribution.php
CRM/Financial/BAO/Order.php
api/v3/Order.php
tests/phpunit/CRM/Contribute/BAO/ContributionTest.php
tests/phpunit/api/v3/ContributionTest.php

index 131a9fa4004614fc2a3909487d94e22091d7f6f5..52bd39b095bf3f070c036e15bbf94433848497d9 100644 (file)
@@ -194,7 +194,9 @@ class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution {
       $params['tax_amount'] = $taxAmount;
       $params['total_amount'] = $taxAmount + $lineTotal;
     }
-    if (isset($params['tax_amount']) && $params['tax_amount'] != $taxAmount && empty($params['skipLineItem'])) {
+    if (isset($params['tax_amount']) && empty($params['skipLineItem'])
+      && !CRM_Utils_Money::equals($params['tax_amount'], $taxAmount, ($params['currency'] ?? Civi::settings()->get('defaultCurrency')))
+    ) {
       CRM_Core_Error::deprecatedWarning('passing in incorrect tax amounts is deprecated');
     }
 
@@ -4401,9 +4403,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac
 
     foreach ($params['line_items'] as &$lineItems) {
       foreach ($lineItems['line_item'] as &$item) {
-        if (empty($item['financial_type_id'])) {
-          $item['financial_type_id'] = $params['financial_type_id'];
-        }
         $lineItemAmount += $item['line_total'] + ($item['tax_amount'] ?? 0.00);
       }
     }
index 9f5df990e5aa9422573a7cd9fd1b7e9787507998..00e0cc02c1dc54947791db59b77c1a377a85009d 100644 (file)
@@ -58,6 +58,29 @@ class CRM_Financial_BAO_Order {
    */
   protected $overrideFinancialTypeID;
 
+  /**
+   * Financial type id to use for any lines where is is not provided.
+   *
+   * @var int
+   */
+  protected $defaultFinancialTypeID;
+
+  /**
+   * @return int
+   */
+  public function getDefaultFinancialTypeID(): int {
+    return $this->defaultFinancialTypeID;
+  }
+
+  /**
+   * Set the default financial type id to be used when the line has none.
+   *
+   * @param int|null $defaultFinancialTypeID
+   */
+  public function setDefaultFinancialTypeID(?int $defaultFinancialTypeID): void {
+    $this->defaultFinancialTypeID = $defaultFinancialTypeID;
+  }
+
   /**
    * Override for the total amount of the order.
    *
@@ -634,4 +657,107 @@ class CRM_Financial_BAO_Order {
     }
   }
 
+  /**
+   * Set the line item.
+   *
+   * This function augments the line item where possible. The calling code
+   * should not attempt to set taxes. This function allows minimal values
+   * to be passed for the default price sets - ie if only membership_type_id is
+   * specified the price_field_id and price_value_id will be determined.
+   *
+   * @param array $lineItem
+   * @param int|string $index
+   *
+   * @throws \API_Exception
+   * @internal tested core code usage only.
+   * @internal use in tested core code only.
+   *
+   */
+  public function setLineItem(array $lineItem, $index): void {
+    if (!empty($lineItem['price_field_id']) && !isset($this->priceSetID)) {
+      $this->setPriceSetIDFromSelectedField($lineItem['price_field_id']);
+    }
+    if (!isset($lineItem['financial_type_id'])) {
+      $lineItem['financial_type_id'] = $this->getDefaultFinancialTypeID();
+    }
+    if (!is_numeric($lineItem['financial_type_id'])) {
+      $lineItem['financial_type_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', $lineItem['financial_type_id']);
+    }
+    $lineItem['tax_amount'] = ($this->getTaxRate($lineItem['financial_type_id']) / 100) * $lineItem['line_total'];
+    if (!empty($lineItem['membership_type_id'])) {
+      $lineItem['entity_table'] = 'civicrm_membership';
+      if (empty($lineItem['price_field_id']) && empty($lineItem['price_field_value_id'])) {
+        // If only the membership type is passed in we use the default price field.
+        if (!isset($this->priceSetID)) {
+          $this->setPriceSetToDefault('membership');
+        }
+        $lineItem = $this->fillMembershipLine($lineItem);
+      }
+    }
+    $this->lineItems[$index] = $lineItem;
+  }
+
+  /**
+   * Set a value on a line item.
+   *
+   * @internal only use in core tested code.
+   *
+   * @param string $name
+   * @param mixed $value
+   * @param string|int $index
+   */
+  public function setLineItemValue(string $name, $value, $index): void {
+    $this->lineItems[$index][$name] = $value;
+  }
+
+  /**
+   * @param int|string $index
+   *
+   * @return string
+   */
+  public function getLineItemEntity($index):string {
+    // @todo - ensure entity_table is set in setLineItem, go back to enotices here.
+    return str_replace('civicrm_', '', ($this->lineItems[$index]['entity_table'] ?? 'contribution'));
+  }
+
+  /**
+   * Get the ordered line item.
+   *
+   * @param string|int $index
+   *
+   * @return array
+   */
+  public function getLineItem($index): array {
+    return $this->lineItems[$index];
+  }
+
+  /**
+   * Fills in additional data for the membership line.
+   *
+   * The minimum requirement is the membership_type_id and that priceSetID is set.
+   *
+   * @param array $lineItem
+   *
+   * @return array
+   */
+  protected function fillMembershipLine(array $lineItem): array {
+    $fields = $this->getPriceFieldsMetadata();
+    $field = reset($fields);
+    if (!isset($lineItem['price_field_value_id'])) {
+      foreach ($field['options'] as $option) {
+        if ((int) $option['membership_type_id'] === (int) $lineItem['membership_type_id']) {
+          $lineItem['price_field_id'] = $field['id'];
+          $lineItem['price_field_value_id'] = $option['id'];
+          $lineItem['qty'] = 1;
+        }
+      }
+    }
+    $option = $field['options'][$lineItem['price_field_value_id']];
+    $lineItem['unit_price'] = $lineItem['line_total'] ?? $option['amount'];
+    $lineItem['label'] = $lineItem['label'] ?? $option['label'];
+    $lineItem['field_title'] = $lineItem['field_title'] ?? $option['label'];
+    $lineItem['financial_type_id'] = $lineItem['financial_type_id'] ?: ($this->getDefaultFinancialTypeID() ?? $option['financial_type_id']);
+    return $lineItem;
+  }
+
 }
index 528a25ef38c5c932a9a785b78a1e636caf50fc0a..7ec2ae93b1b52437d76eb4de3180df223cc22244 100644 (file)
@@ -76,22 +76,20 @@ function civicrm_api3_order_create(array $params): array {
   $entity = NULL;
   $entityIds = [];
   $params['contribution_status_id'] = 'Pending';
-  $priceSetID = NULL;
+  $order = new CRM_Financial_BAO_Order();
+  $order->setDefaultFinancialTypeID($params['financial_type_id'] ?? NULL);
 
   if (!empty($params['line_items']) && is_array($params['line_items'])) {
     CRM_Contribute_BAO_Contribution::checkLineItems($params);
-    foreach ($params['line_items'] as $lineItems) {
-      $entityParams = $lineItems['params'] ?? [];
-      if (!empty($entityParams) && !empty($lineItems['line_item'])) {
-        $item = reset($lineItems['line_item']);
-        if (!empty($item['membership_type_id'])) {
-          $entity = 'membership';
-        }
-        else {
-          $entity = str_replace('civicrm_', '', $item['entity_table']);
-        }
+    foreach ($params['line_items'] as $index => $lineItems) {
+      foreach ($lineItems['line_item'] as $innerIndex => $lineItem) {
+        $lineIndex = $index . '+' . $innerIndex;
+        $order->setLineItem($lineItem, $lineIndex);
       }
 
+      $entityParams = $lineItems['params'] ?? [];
+      $entity = $order->getLineItemEntity($lineIndex);
+
       if ($entityParams) {
         $supportedEntity = TRUE;
         switch ($entity) {
@@ -118,23 +116,20 @@ function civicrm_api3_order_create(array $params): array {
           $entityResult = civicrm_api3($entity, 'create', $entityParams);
           $params['contribution_mode'] = $entity;
           $entityIds[] = $params[$entity . '_id'] = $entityResult['id'];
-          foreach ($lineItems['line_item'] as &$items) {
-            $items['entity_id'] = $entityResult['id'];
+          foreach ($lineItems['line_item'] as $innerIndex => $lineItem) {
+            $lineIndex = $index . '+' . $innerIndex;
+            $order->setLineItemValue('entity_id', $entityResult['id'], $lineIndex);
           }
         }
       }
-
-      if (empty($priceSetID)) {
-        $item = reset($lineItems['line_item']);
-        $priceSetID = (int) civicrm_api3('PriceField', 'getvalue', [
-          'return' => 'price_set_id',
-          'id' => $item['price_field_id'],
-        ]);
-        $params['line_item'][$priceSetID] = [];
-      }
-      $params['line_item'][$priceSetID] = array_merge($params['line_item'][$priceSetID], $lineItems['line_item']);
     }
+    $priceSetID = $order->getPriceSetID();
+    $params['line_item'][$priceSetID] = $order->getLineItems();
   }
+  else {
+    $order->setPriceSetToDefault('contribution');
+  }
+
   $contributionParams = $params;
   // If this is nested we need to set sequential to 0 as sequential handling is done
   // in create_success & id will be miscalculated...
@@ -149,7 +144,7 @@ function civicrm_api3_order_create(array $params): array {
   }
 
   $contribution = civicrm_api3('Contribution', 'create', $contributionParams);
-  $contribution['values'][$contribution['id']]['line_item'] = $params['line_item'][$priceSetID] ?? [];
+  $contribution['values'][$contribution['id']]['line_item'] = $order->getLineItems();
 
   // add payments
   if ($entity && !empty($contribution['id'])) {
index 63edf813c8a6aea91a6d10836ef15a78c3ff1eda..7141f17f89b4aff4290f008e8be3eb421f7f540e 100644 (file)
@@ -820,8 +820,6 @@ WHERE eft.entity_id = %1 AND ft.to_financial_account_id <> %2";
         $e->getMessage()
       );
     }
-
-    $this->assertEquals(3, $params['line_items'][0]['line_item'][0]['financial_type_id']);
     $params['total_amount'] = 300;
 
     CRM_Contribute_BAO_Contribution::checkLineItems($params);
index e57891b89ea16c4913c6744ef394aaf091bce1f3..5ad26b09bedfc67e985414517d8f546fe38a7527 100644 (file)
@@ -4345,24 +4345,31 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
       'payment_processor_id' => $this->paymentProcessorID,
     ], $generalParams, $recurParams));
 
-    $this->callAPISuccess('membership', 'create', [
-      'contact_id' => $newContact['id'],
-      'contribution_recur_id' => $contributionRecur['id'],
-      'financial_type_id' => 'Member Dues',
-      'membership_type_id' => $membershipType['id'],
-      'num_terms' => 1,
-      'skipLineItem' => TRUE,
-    ]);
-
-    CRM_Price_BAO_LineItem::getLineItemArray($this->_params, NULL, 'membership', $membershipType['id']);
-    $originalContribution = $this->callAPISuccess('contribution', 'create', array_merge(
+    $originalContribution = $this->callAPISuccess('Order', 'create', array_merge(
       $this->_params,
       [
         'contact_id' => $newContact['id'],
         'contribution_recur_id' => $contributionRecur['id'],
         'financial_type_id' => 'Member Dues',
-        'contribution_status_id' => 1,
+        'api.Payment.create' => ['total_amount' => 100, 'payment_instrument_id' => 'Credit card'],
         'invoice_id' => 2345,
+        'line_items' => [
+          [
+            'line_item' => [
+              [
+                'membership_type_id' => $membershipType['id'],
+                'financial_type_id' => 'Member Dues',
+                'line_total' => $generalParams['total_amount'] ?? 100,
+              ],
+            ],
+            'params' => [
+              'contact_id' => $newContact['id'],
+              'contribution_recur_id' => $contributionRecur['id'],
+              'membership_type_id' => $membershipType['id'],
+              'num_terms' => 1,
+            ],
+          ],
+        ],
       ], $generalParams)
     );
     $lineItem = $this->callAPISuccess('LineItem', 'getsingle', []);