Merge pull request #23537 from eileenmcnaughton/greet_cust
authorcolemanw <coleman@civicrm.org>
Mon, 23 May 2022 20:44:54 +0000 (16:44 -0400)
committerGitHub <noreply@github.com>
Mon, 23 May 2022 20:44:54 +0000 (16:44 -0400)
Greeting handling - if email_greeting_custom (etc) isset & email_greeting_id is not, set to 'Customized'

18 files changed:
CRM/Contact/Form/Merge.php
CRM/Contact/Import/Parser/Contact.php
CRM/Contribute/BAO/ContributionRecur.php
CRM/Core/Controller.php
CRM/Event/DAO/Event.php
Civi/Api4/Generic/DAOGetFieldsAction.php
api/v3/Contribution.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/GetSearchTasks.php
ext/search_kit/ang/crmSearchTasks/crmSearchTasks.component.js
ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php
ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunWithCustomFieldTest.php
js/crm.ajax.js
tests/phpunit/CRM/Contact/Import/Form/data/individual_locations_with_related.csv [new file with mode: 0644]
tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php
tests/phpunit/CRM/Contribute/Import/Parser/data/.~lock.contributions.csv# [deleted file]
tests/phpunit/api/v3/ContributionTest.php
xml/schema/Event/Event.xml

index 422806e64b733a3d3220b58621caf1942e7db40a..0dca67a00989b5cbbfa4819c37c09bab17f7242e 100644 (file)
@@ -309,7 +309,17 @@ class CRM_Contact_Form_Merge extends CRM_Core_Form {
 
     $formValues['main_details'] = $this->_mainDetails;
     $formValues['other_details'] = $this->_otherDetails;
+
+    // Check if any rel_tables checkboxes have been de-selected
+    $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($this->_cid, $this->_oid);
+    // If rel_tables is not set then initialise with 0 value, required for the check which calls removeContactBelongings in moveAllBelongings
+    foreach (array_keys($rowsElementsAndInfo['rel_tables']) as $relTableElement) {
+      if (!array_key_exists($relTableElement, $formValues)) {
+        $formValues[$relTableElement] = '0';
+      }
+    }
     $migrationData = ['migration_info' => $formValues];
+
     CRM_Utils_Hook::merge('form', $migrationData, $this->_cid, $this->_oid);
     CRM_Dedupe_Merger::moveAllBelongings($this->_cid, $this->_oid, $migrationData['migration_info']);
 
index 8f8a2b191c025c42795a361a2ff947b02a7b677d..1975bba86cd5f822e07c16d8c27e3c9b2840998f 100644 (file)
@@ -1498,7 +1498,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
       $formatted['updateBlankLocInfo'] = FALSE;
     }
 
-    [$data, $contactDetails] = CRM_Contact_BAO_Contact::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
+    [$data, $contactDetails] = $this->formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
 
     // manage is_opt_out
     if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
@@ -1542,6 +1542,401 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
     return $newContact;
   }
 
+  /**
+   * Legacy format profile contact parameters.
+   *
+   * This is a formerly shared function - most of the stuff in it probably does
+   * nothing but copied here to star unravelling that...
+   *
+   * @param array $params
+   * @param array $fields
+   * @param int|null $contactID
+   * @param int|null $ufGroupId
+   * @param string|null $ctype
+   * @param bool $skipCustom
+   *
+   * @return array
+   */
+  private function formatProfileContactParams(
+    &$params,
+    $fields,
+    $contactID = NULL,
+    $ufGroupId = NULL,
+    $ctype = NULL,
+    $skipCustom = FALSE
+  ) {
+
+    $data = $contactDetails = [];
+
+    // get the contact details (hier)
+    if ($contactID) {
+      $details = CRM_Contact_BAO_Contact::getHierContactDetails($contactID, $fields);
+
+      $contactDetails = $details[$contactID];
+      $data['contact_type'] = $contactDetails['contact_type'] ?? NULL;
+      $data['contact_sub_type'] = $contactDetails['contact_sub_type'] ?? NULL;
+    }
+    else {
+      //we should get contact type only if contact
+      if ($ufGroupId) {
+        $data['contact_type'] = CRM_Core_BAO_UFField::getProfileType($ufGroupId, TRUE, FALSE, TRUE);
+
+        //special case to handle profile with only contact fields
+        if ($data['contact_type'] == 'Contact') {
+          $data['contact_type'] = 'Individual';
+        }
+        elseif (CRM_Contact_BAO_ContactType::isaSubType($data['contact_type'])) {
+          $data['contact_type'] = CRM_Contact_BAO_ContactType::getBasicType($data['contact_type']);
+        }
+      }
+      elseif ($ctype) {
+        $data['contact_type'] = $ctype;
+      }
+      else {
+        $data['contact_type'] = 'Individual';
+      }
+    }
+
+    //fix contact sub type CRM-5125
+    if (array_key_exists('contact_sub_type', $params) &&
+      !empty($params['contact_sub_type'])
+    ) {
+      $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type']);
+    }
+    elseif (array_key_exists('contact_sub_type_hidden', $params) &&
+      !empty($params['contact_sub_type_hidden'])
+    ) {
+      // if profile was used, and had any subtype, we obtain it from there
+      //CRM-13596 - add to existing contact types, rather than overwriting
+      if (empty($data['contact_sub_type'])) {
+        // If we don't have a contact ID the $data['contact_sub_type'] will not be defined...
+        $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']);
+      }
+      else {
+        $data_contact_sub_type_arr = CRM_Utils_Array::explodePadded($data['contact_sub_type']);
+        if (!in_array($params['contact_sub_type_hidden'], $data_contact_sub_type_arr)) {
+          //CRM-20517 - make sure contact_sub_type gets the correct delimiters
+          $data['contact_sub_type'] = trim($data['contact_sub_type'], CRM_Core_DAO::VALUE_SEPARATOR);
+          $data['contact_sub_type'] = CRM_Core_DAO::VALUE_SEPARATOR . $data['contact_sub_type'] . CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']);
+        }
+      }
+    }
+
+    if ($ctype == 'Organization') {
+      $data['organization_name'] = $contactDetails['organization_name'] ?? NULL;
+    }
+    elseif ($ctype == 'Household') {
+      $data['household_name'] = $contactDetails['household_name'] ?? NULL;
+    }
+
+    $locationType = [];
+    $count = 1;
+
+    if ($contactID) {
+      //add contact id
+      $data['contact_id'] = $contactID;
+      $primaryLocationType = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID);
+    }
+    else {
+      $defaultLocation = CRM_Core_BAO_LocationType::getDefault();
+      $defaultLocationId = $defaultLocation->id;
+    }
+
+    $billingLocationTypeId = CRM_Core_BAO_LocationType::getBilling();
+
+    $blocks = ['email', 'phone', 'im', 'openid'];
+
+    $multiplFields = ['url'];
+    // prevent overwritten of formatted array, reset all block from
+    // params if it is not in valid format (since import pass valid format)
+    foreach ($blocks as $blk) {
+      if (array_key_exists($blk, $params) &&
+        !is_array($params[$blk])
+      ) {
+        unset($params[$blk]);
+      }
+    }
+
+    $primaryPhoneLoc = NULL;
+    $session = CRM_Core_Session::singleton();
+    foreach ($params as $key => $value) {
+      [$fieldName, $locTypeId, $typeId] = CRM_Utils_System::explode('-', $key, 3);
+
+      if ($locTypeId == 'Primary') {
+        if ($contactID) {
+          if (in_array($fieldName, $blocks)) {
+            $locTypeId = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID, FALSE, $fieldName);
+          }
+          else {
+            $locTypeId = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID, FALSE, 'address');
+          }
+          $primaryLocationType = $locTypeId;
+        }
+        else {
+          $locTypeId = $defaultLocationId;
+        }
+      }
+
+      if (is_numeric($locTypeId) &&
+        !in_array($fieldName, $multiplFields) &&
+        substr($fieldName, 0, 7) != 'custom_'
+      ) {
+        $index = $locTypeId;
+
+        if (is_numeric($typeId)) {
+          $index .= '-' . $typeId;
+        }
+        if (!in_array($index, $locationType)) {
+          $locationType[$count] = $index;
+          $count++;
+        }
+
+        $loc = CRM_Utils_Array::key($index, $locationType);
+
+        $blockName = $this->getLocationEntityForKey($fieldName);
+
+        $data[$blockName][$loc]['location_type_id'] = $locTypeId;
+
+        //set is_billing true, for location type "Billing"
+        if ($locTypeId == $billingLocationTypeId) {
+          $data[$blockName][$loc]['is_billing'] = 1;
+        }
+
+        if ($contactID) {
+          //get the primary location type
+          if ($locTypeId == $primaryLocationType) {
+            $data[$blockName][$loc]['is_primary'] = 1;
+          }
+        }
+        elseif ($locTypeId == $defaultLocationId) {
+          $data[$blockName][$loc]['is_primary'] = 1;
+        }
+
+        if (in_array($fieldName, ['phone'])) {
+          if ($typeId) {
+            $data['phone'][$loc]['phone_type_id'] = $typeId;
+          }
+          else {
+            $data['phone'][$loc]['phone_type_id'] = '';
+          }
+          $data['phone'][$loc]['phone'] = $value;
+
+          //special case to handle primary phone with different phone types
+          // in this case we make first phone type as primary
+          if (isset($data['phone'][$loc]['is_primary']) && !$primaryPhoneLoc) {
+            $primaryPhoneLoc = $loc;
+          }
+
+          if ($loc != $primaryPhoneLoc) {
+            unset($data['phone'][$loc]['is_primary']);
+          }
+        }
+        elseif ($fieldName == 'email') {
+          $data['email'][$loc]['email'] = $value;
+          if (empty($contactID)) {
+            $data['email'][$loc]['is_primary'] = 1;
+          }
+        }
+        elseif ($fieldName == 'im') {
+          if (isset($params[$key . '-provider_id'])) {
+            $data['im'][$loc]['provider_id'] = $params[$key . '-provider_id'];
+          }
+          if (strpos($key, '-provider_id') !== FALSE) {
+            $data['im'][$loc]['provider_id'] = $params[$key];
+          }
+          else {
+            $data['im'][$loc]['name'] = $value;
+          }
+        }
+        elseif ($fieldName == 'openid') {
+          $data['openid'][$loc]['openid'] = $value;
+        }
+        else {
+          if ($fieldName === 'state_province') {
+            // CRM-3393
+            if (is_numeric($value) && ((int ) $value) >= 1000) {
+              $data['address'][$loc]['state_province_id'] = $value;
+            }
+            elseif (empty($value)) {
+              $data['address'][$loc]['state_province_id'] = '';
+            }
+            else {
+              $data['address'][$loc]['state_province'] = $value;
+            }
+          }
+          elseif ($fieldName === 'country') {
+            // CRM-3393
+            if (is_numeric($value) && ((int ) $value) >= 1000
+            ) {
+              $data['address'][$loc]['country_id'] = $value;
+            }
+            elseif (empty($value)) {
+              $data['address'][$loc]['country_id'] = '';
+            }
+            else {
+              $data['address'][$loc]['country'] = $value;
+            }
+          }
+          elseif ($fieldName === 'county') {
+            $data['address'][$loc]['county_id'] = $value;
+          }
+          elseif ($fieldName == 'address_name') {
+            $data['address'][$loc]['name'] = $value;
+          }
+          elseif (substr($fieldName, 0, 14) === 'address_custom') {
+            $data['address'][$loc][substr($fieldName, 8)] = $value;
+          }
+          else {
+            $data[$blockName][$loc][$fieldName] = $value;
+          }
+        }
+      }
+      else {
+        if (substr($key, 0, 4) === 'url-') {
+          $websiteField = explode('-', $key);
+          $data['website'][$websiteField[1]]['website_type_id'] = $websiteField[1];
+          $data['website'][$websiteField[1]]['url'] = $value;
+        }
+        elseif (in_array($key, CRM_Contact_BAO_Contact::$_greetingTypes, TRUE)) {
+          //save email/postal greeting and addressee values if any, CRM-4575
+          $data[$key . '_id'] = $value;
+        }
+        elseif (!$skipCustom && ($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key))) {
+          // for autocomplete transfer hidden value instead of label
+          if ($params[$key] && isset($params[$key . '_id'])) {
+            $value = $params[$key . '_id'];
+          }
+
+          // we need to append time with date
+          if ($params[$key] && isset($params[$key . '_time'])) {
+            $value .= ' ' . $params[$key . '_time'];
+          }
+
+          // if auth source is not checksum / login && $value is blank, do not proceed - CRM-10128
+          if (($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 &&
+            ($value == '' || !isset($value))
+          ) {
+            continue;
+          }
+
+          $valueId = NULL;
+          if (!empty($params['customRecordValues'])) {
+            if (is_array($params['customRecordValues']) && !empty($params['customRecordValues'])) {
+              foreach ($params['customRecordValues'] as $recId => $customFields) {
+                if (is_array($customFields) && !empty($customFields)) {
+                  foreach ($customFields as $customFieldName) {
+                    if ($customFieldName == $key) {
+                      $valueId = $recId;
+                      break;
+                    }
+                  }
+                }
+              }
+            }
+          }
+
+          //CRM-13596 - check for contact_sub_type_hidden first
+          if (array_key_exists('contact_sub_type_hidden', $params)) {
+            $type = $params['contact_sub_type_hidden'];
+          }
+          else {
+            $type = $data['contact_type'];
+            if (!empty($data['contact_sub_type'])) {
+              $type = CRM_Utils_Array::explodePadded($data['contact_sub_type']);
+            }
+          }
+
+          CRM_Core_BAO_CustomField::formatCustomField($customFieldId,
+            $data['custom'],
+            $value,
+            $type,
+            $valueId,
+            $contactID,
+            FALSE,
+            FALSE
+          );
+        }
+        elseif ($key === 'edit') {
+          continue;
+        }
+        else {
+          if ($key === 'location') {
+            foreach ($value as $locationTypeId => $field) {
+              foreach ($field as $block => $val) {
+                if ($block === 'address' && array_key_exists('address_name', $val)) {
+                  $value[$locationTypeId][$block]['name'] = $value[$locationTypeId][$block]['address_name'];
+                }
+              }
+            }
+          }
+          if ($key === 'phone' && isset($params['phone_ext'])) {
+            $data[$key] = $value;
+            foreach ($value as $cnt => $phoneBlock) {
+              if ($params[$key][$cnt]['location_type_id'] == $params['phone_ext'][$cnt]['location_type_id']) {
+                $data[$key][$cnt]['phone_ext'] = CRM_Utils_Array::retrieveValueRecursive($params['phone_ext'][$cnt], 'phone_ext');
+              }
+            }
+          }
+          elseif (in_array($key, ['nick_name', 'job_title', 'middle_name', 'birth_date', 'gender_id', 'current_employer', 'prefix_id', 'suffix_id'])
+            && ($value == '' || !isset($value)) &&
+            ($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 ||
+            ($key === 'current_employer' && empty($params['current_employer']))) {
+            // CRM-10128: if auth source is not checksum / login && $value is blank, do not fill $data with empty value
+            // to avoid update with empty values
+            continue;
+          }
+          else {
+            $data[$key] = $value;
+          }
+        }
+      }
+    }
+
+    if (!isset($data['contact_type'])) {
+      $data['contact_type'] = 'Individual';
+    }
+
+    //set the values for checkboxes (do_not_email, do_not_mail, do_not_trade, do_not_phone)
+    $privacy = CRM_Core_SelectValues::privacy();
+    foreach ($privacy as $key => $value) {
+      if (array_key_exists($key, $fields)) {
+        // do not reset values for existing contacts, if fields are added to a profile
+        if (array_key_exists($key, $params)) {
+          $data[$key] = $params[$key];
+          if (empty($params[$key])) {
+            $data[$key] = 0;
+          }
+        }
+        elseif (!$contactID) {
+          $data[$key] = 0;
+        }
+      }
+    }
+
+    return [$data, $contactDetails];
+  }
+
+  /**
+   * Get the relevant location entity for the array key.
+   *
+   * Based on the field name we determine which location entity
+   * we are dealing with. Apart from a few specific ones they
+   * are mostly 'address' (the default).
+   *
+   * @param string $fieldName
+   *
+   * @return string
+   */
+  private static function getLocationEntityForKey($fieldName) {
+    if (in_array($fieldName, ['email', 'phone', 'im', 'openid'])) {
+      return $fieldName;
+    }
+    if ($fieldName === 'phone_ext') {
+      return 'phone';
+    }
+    return 'address';
+  }
+
   /**
    * Format params for update and fill mode.
    *
@@ -2590,7 +2985,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
       $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']);
     }
     if ($mappedField['im_provider_id']) {
-      $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['provider_id']);
+      $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['im_provider_id']);
     }
     return implode(' - ', $title);
   }
@@ -2905,7 +3300,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
     // Time to see if we can find an existing contact ID to make this an update
     // not a create.
     if ($extIDMatch || $this->isUpdateExistingContacts()) {
-      return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id'));
+      return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id') ?: NULL);
     }
     return NULL;
   }
index e68e08d17962de63b20c1acbecddb24cd6dd69a6..5928599940e9db7b9a19533446ca13e2a37fcd55 100644 (file)
@@ -489,7 +489,7 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
    *
    * @param int $id
    * @param array $overrides
-   *   Parameters that should be overriden. Add unit tests if using parameters other than total_amount & financial_type_id.
+   *   Parameters that should be overridden. Add unit tests if using parameters other than total_amount & financial_type_id.
    *
    * @return array
    *
index 7b0a5a4773627b7a40e35fe0441ed082445c796a..f31e5f963f72dca2c8d27fb3c47e9d4b57ed7b46 100644 (file)
@@ -300,10 +300,7 @@ class CRM_Core_Controller extends HTML_QuickForm_Controller {
     // https://github.com/civicrm/civicrm-core/pull/17324
     // and/or related get merged, then we should remove the REQUEST reference here.
     $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL;
-    // Allow POST if `$_GET['reset'] == 1` because standalone search actions require a
-    // (potentially large) amount of data to the server and must make the page request using POST.
-    // See https://lab.civicrm.org/dev/core/-/issues/3222
-    if (!$key && (!empty($_GET['reset']) || in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD']))) {
+    if (!$key && in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'])) {
       // Generate a key if this is an initial request without one.
       // We allow HEAD here because it is used by bots to validate URLs, so if
       // we issue a 500 server error to them they may think the site is broken.
index cfded17014a7678d9f01259e7fc692fec89928aa..b949ec5b81d26f80569c7fb81f613588f41b3eb3 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Event/Event.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:8e47b6d674b9aa18013e67f27b4b355d)
+ * (GenCodeChecksum:ddad900cfc0f303d651fa7b935157992)
  */
 
 /**
@@ -1112,7 +1112,7 @@ class CRM_Event_DAO_Event extends CRM_Core_DAO {
         'is_show_location' => [
           'name' => 'is_show_location',
           'type' => CRM_Utils_Type::T_BOOLEAN,
-          'title' => ts('show location'),
+          'title' => ts('Show Location'),
           'description' => ts('If true, show event location.'),
           'required' => TRUE,
           'where' => 'civicrm_event.is_show_location',
index e877e6dd8b2b852bc0ff654167fb76fce3386b81..77a97e82ec066288d48e2e1b219993cea3530f3d 100644 (file)
@@ -111,7 +111,7 @@ class DAOGetFieldsAction extends BasicGetFieldsAction {
           throw new \API_Exception('Illegal expression');
         }
         $baoName = CoreUtil::getBAOFromApiName($this->getEntityName());
-        $options = $baoName::buildOptions($fieldName, $context);
+        $options = $baoName::buildOptions($fieldName, $context) ?: [];
         $this->values[$fieldName] = FormattingUtil::replacePseudoconstant($options, $this->values[$key], TRUE);
         unset($this->values[$key]);
       }
index b4205cd3c14e4b4f0f1e229acfc5b5bd9fa4890e..6000cd96aec7ac27f728133193ab249832234eac 100644 (file)
@@ -618,14 +618,11 @@ function _civicrm_api3_contribution_completetransaction_spec(&$params) {
 function civicrm_api3_contribution_repeattransaction($params) {
   civicrm_api3_verify_one_mandatory($params, NULL, ['contribution_recur_id', 'original_contribution_id']);
   if (empty($params['original_contribution_id'])) {
-    //  CRM-19873 call with test mode.
-    $params['original_contribution_id'] = civicrm_api3('contribution', 'getvalue', [
-      'return' => 'id',
-      'contribution_status_id' => ['IN' => ['Completed']],
-      'contribution_recur_id' => $params['contribution_recur_id'],
-      'contribution_test' => CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur', $params['contribution_recur_id'], 'is_test'),
-      'options' => ['limit' => 1, 'sort' => 'id DESC'],
-    ]);
+    $templateContribution = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($params['contribution_recur_id']);
+    if (empty($templateContribution)) {
+      throw new CiviCRM_API3_Exception('Contribution.repeattransaction failed to get original_contribution_id for recur with ID: ' . $params['contribution_recur_id']);
+    }
+    $params['original_contribution_id'] = $templateContribution['id'];
   }
   $contribution = new CRM_Contribute_BAO_Contribution();
   $contribution->id = $params['original_contribution_id'];
index 1b592602a7873a69ecd07fb2992f0d6f4c96b462..7a60d61ffe31c381bd992e1b7459eee53883d9f8 100644 (file)
@@ -7,6 +7,7 @@ use Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
 use Civi\Api4\Query\SqlField;
 use Civi\Api4\SearchDisplay;
 use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\FormattingUtil;
 
 /**
  * Base class for running a search.
@@ -535,8 +536,8 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
   }
 
   /**
-   * @param $column
-   * @param $data
+   * @param array $column
+   * @param array $data
    * @return array{entity: string, action: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, record: array, value: mixed}|null
    */
   private function formatEditableColumn($column, $data) {
@@ -547,6 +548,12 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
       $editable['action'] = 'update';
       $editable['record'][$editable['id_key']] = $data[$editable['id_path']];
       $editable['value'] = $data[$editable['value_path']];
+      // Ensure field is appropriate to this entity sub-type
+      $field = $this->getField($column['key']);
+      $entityValues = FormattingUtil::filterByPrefix($data, $editable['id_path'], $editable['id_key']);
+      if (!$this->fieldBelongsToEntity($editable['entity'], $field['name'], $entityValues)) {
+        return NULL;
+      }
     }
     // Generate params to create new record, if applicable
     elseif ($editable['explicit_join'] && !$this->getJoin($editable['explicit_join'])['bridge']) {
@@ -600,18 +607,45 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
         'values' => $editable['record'],
       ], 0)['access'];
       if ($access) {
-        \CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'value_path', 'explicit_join');
+        // Remove info that's for internal use only
+        \CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'value_path', 'explicit_join', 'grouping_fields');
         return $editable;
       }
     }
     return NULL;
   }
 
+  /**
+   * Check if a field is appropriate for this entity type or sub-type.
+   *
+   * For example, the 'first_name' field does not belong to Contacts of type Organization.
+   * And custom data is sometimes limited to specific contact types, event types, case types, etc.
+   *
+   * @param string $entityName
+   * @param string $fieldName
+   * @param array $entityValues
+   * @param bool $checkPermissions
+   * @return bool
+   */
+  private function fieldBelongsToEntity($entityName, $fieldName, $entityValues, $checkPermissions = TRUE) {
+    try {
+      return (bool) civicrm_api4($entityName, 'getFields', [
+        'checkPermissions' => $checkPermissions,
+        'where' => [['name', '=', $fieldName]],
+        'values' => $entityValues,
+      ])->count();
+    }
+    catch (\API_Exception $e) {
+      return FALSE;
+    }
+  }
+
   /**
    * @param $key
-   * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string, explicit_join: string}|null
+   * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string, explicit_join: string, grouping_fields: array}|null
    */
   private function getEditableInfo($key) {
+    $result = NULL;
     // Strip pseudoconstant suffix
     [$key] = explode(':', $key);
     $field = $this->getField($key);
@@ -621,13 +655,14 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
     }
     if ($field) {
       $idKey = CoreUtil::getIdFieldName($field['entity']);
-      $idPath = ($field['explicit_join'] ? $field['explicit_join'] . '.' : '') . $idKey;
+      $path = ($field['explicit_join'] ? $field['explicit_join'] . '.' : '');
+      $idPath = $path . $idKey;
       // Hack to support editing relationships
       if ($field['entity'] === 'RelationshipCache') {
         $field['entity'] = 'Relationship';
-        $idPath = ($field['explicit_join'] ? $field['explicit_join'] . '.' : '') . 'relationship_id';
+        $idPath = $path . 'relationship_id';
       }
-      return [
+      $result = [
         'entity' => $field['entity'],
         'input_type' => $field['input_type'],
         'data_type' => $field['data_type'],
@@ -640,9 +675,18 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
         'id_key' => $idKey,
         'id_path' => $idPath,
         'explicit_join' => $field['explicit_join'],
+        'grouping_fields' => [],
       ];
+      // Grouping fields get added to the query so that contact sub-type and entity type (for custom fields)
+      // are available to filter fields specific to an entity sub-type. See self::fieldBelongsToEntity()
+      if ($field['type'] === 'Custom' || $field['entity'] === 'Contact') {
+        $customInfo = \Civi\Api4\Utils\CoreUtil::getCustomGroupExtends($field['entity']);
+        foreach ((array) ($customInfo['grouping'] ?? []) as $grouping) {
+          $result['grouping_fields'][] = $path . $grouping;
+        }
+      }
     }
-    return NULL;
+    return $result;
   }
 
   /**
@@ -937,8 +981,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
       if (!empty($column['editable'])) {
         $editable = $this->getEditableInfo($column['key']);
         if ($editable) {
-          $additions[] = $editable['value_path'];
-          $additions[] = $editable['id_path'];
+          $additions = array_merge($additions, $editable['grouping_fields'], [$editable['value_path'], $editable['id_path']]);
         }
       }
       // Add style & icon conditions for the column
index 97286984e3775b3e273255f44b7b262dff9590b7..b0ded1f45eeecf966080b0e74463cb9e47202e22 100644 (file)
@@ -41,8 +41,7 @@ class GetSearchTasks extends \Civi\Api4\Generic\AbstractAction {
         'icon' => 'fa-file-excel-o',
         'crmPopup' => [
           'path' => "'civicrm/export/standalone'",
-          'query' => "{reset: 1, entity: '{$entity['name']}'}",
-          'data' => "{id: ids.join(',')}",
+          'query' => "{reset: 1, entity: '{$entity['name']}', id: ids.join(',')}",
         ],
       ];
     }
@@ -105,8 +104,7 @@ class GetSearchTasks extends \Civi\Api4\Generic\AbstractAction {
             'icon' => $task['icon'] ?? 'fa-gear',
             'crmPopup' => [
               'path' => "'{$task['url']}'",
-              'query' => "{reset: 1}",
-              'data' => "{cids: ids.join(',')}",
+              'query' => "{reset: 1, cids: ids.join(',')}",
             ],
           ];
         }
@@ -143,7 +141,7 @@ class GetSearchTasks extends \Civi\Api4\Generic\AbstractAction {
             'icon' => $task['icon'] ?? 'fa-gear',
             'crmPopup' => [
               'path' => "'{$task['url']}'",
-              'data' => "{id: ids.join(',')}",
+              'query' => "{id: ids.join(',')}",
             ],
           ];
         }
index 639bc4cfddf76d6cfd69b9c65563b4ccf9a1cfe6..4910dcdd1944a4ed379b7b3e740c1b66f331fd51 100644 (file)
@@ -67,7 +67,7 @@
         if (action.crmPopup) {
           var path = $scope.$eval(action.crmPopup.path, data),
             query = action.crmPopup.query && $scope.$eval(action.crmPopup.query, data);
-          CRM.loadForm(CRM.url(path, query), {post: action.crmPopup.data && $scope.$eval(action.crmPopup.data, data)})
+          CRM.loadForm(CRM.url(path, query))
             .on('crmFormSuccess', ctrl.refresh);
         }
         // If action uses dialogService
index f5d8deb1b71d2bcd2e62c6c84927cd8d9fda74d0..62e43010b08737c82f3d97edc2159da2b1995891 100644 (file)
@@ -1253,4 +1253,107 @@ class SearchRunTest extends \PHPUnit\Framework\TestCase implements HeadlessInter
     $this->assertEquals([1, 2], $data);
   }
 
+  public function testEditableContactFields() {
+    $source = uniqid(__FUNCTION__);
+    $sampleData = [
+      ['contact_type' => 'Individual', 'first_name' => 'One'],
+      ['contact_type' => 'Individual'],
+      ['contact_type' => 'Organization'],
+      ['contact_type' => 'Household'],
+    ];
+    $contact = Contact::save(FALSE)
+      ->addDefault('source', $source)
+      ->setRecords($sampleData)
+      ->execute();
+
+    $params = [
+      'checkPermissions' => FALSE,
+      'return' => 'page:1',
+      'savedSearch' => [
+        'api_entity' => 'Contact',
+        'api_params' => [
+          'version' => 4,
+          'select' => ['first_name', 'organization_name', 'household_name'],
+          'where' => [['source', '=', $source]],
+        ],
+      ],
+      'display' => [
+        'type' => 'table',
+        'label' => '',
+        'settings' => [
+          'actions' => TRUE,
+          'pager' => [],
+          'columns' => [
+            [
+              'key' => 'first_name',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'editable' => TRUE,
+            ],
+            [
+              'key' => 'organization_name',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'editable' => TRUE,
+            ],
+            [
+              'key' => 'household_name',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'editable' => TRUE,
+            ],
+          ],
+          'sort' => [
+            ['id', 'ASC'],
+          ],
+        ],
+      ],
+      'afform' => NULL,
+    ];
+
+    $result = civicrm_api4('SearchDisplay', 'run', $params);
+    // First Individual
+    $expectedFirstNameEdit = [
+      'entity' => 'Contact',
+      'input_type' => 'Text',
+      'data_type' => 'String',
+      'options' => FALSE,
+      'serialize' => FALSE,
+      'nullable' => TRUE,
+      'fk_entity' => NULL,
+      'value_key' => 'first_name',
+      'record' => ['id' => $contact[0]['id']],
+      'action' => 'update',
+      'value' => 'One',
+    ];
+    // Ensure first_name is editable but not organization_name or household_name
+    $this->assertEquals($expectedFirstNameEdit, $result[0]['columns'][0]['edit']);
+    $this->assertTrue(!isset($result[0]['columns'][1]['edit']));
+    $this->assertTrue(!isset($result[0]['columns'][2]['edit']));
+
+    // Second Individual
+    $expectedFirstNameEdit['record']['id'] = $contact[1]['id'];
+    $expectedFirstNameEdit['value'] = NULL;
+    $this->assertEquals($expectedFirstNameEdit, $result[1]['columns'][0]['edit']);
+    $this->assertTrue(!isset($result[1]['columns'][1]['edit']));
+    $this->assertTrue(!isset($result[1]['columns'][2]['edit']));
+
+    // Third contact: Organization
+    $expectedFirstNameEdit['record']['id'] = $contact[2]['id'];
+    $expectedFirstNameEdit['value_key'] = 'organization_name';
+    $this->assertTrue(!isset($result[2]['columns'][0]['edit']));
+    $this->assertEquals($expectedFirstNameEdit, $result[2]['columns'][1]['edit']);
+    $this->assertTrue(!isset($result[2]['columns'][2]['edit']));
+
+    // Third contact: Household
+    $expectedFirstNameEdit['record']['id'] = $contact[3]['id'];
+    $expectedFirstNameEdit['value_key'] = 'household_name';
+    $this->assertTrue(!isset($result[3]['columns'][0]['edit']));
+    $this->assertTrue(!isset($result[3]['columns'][1]['edit']));
+    $this->assertEquals($expectedFirstNameEdit, $result[3]['columns'][2]['edit']);
+  }
+
 }
index d4d51d1b43675e237cf24fda0d643a5a408955c1..3b5bda71fc124b8e32bc7b29f319c64e3f2f39c7 100644 (file)
@@ -6,6 +6,8 @@ require_once 'tests/phpunit/api/v4/Api4TestBase.php';
 require_once 'tests/phpunit/api/v4/Custom/CustomTestBase.php';
 
 use api\v4\Custom\CustomTestBase;
+use Civi\Api4\Activity;
+use Civi\Api4\Contact;
 use Civi\Api4\CustomField;
 use Civi\Api4\CustomGroup;
 
@@ -246,7 +248,7 @@ class SearchRunWithCustomFieldTest extends CustomTestBase {
     $this->assertEquals('abc', $result[0]['columns'][3]['val']);
     $this->assertEquals($childRel, $result[0]['columns'][3]['edit']['record']['id']);
     $this->assertNull($result[0]['columns'][4]['val']);
-    // $this->assertArrayNotHasKey('edit', $result[0]['columns'][4]);
+    $this->assertArrayNotHasKey('edit', $result[0]['columns'][4]);
 
     // Second contact has a spouse relation but not a child
     $this->assertEquals('s', $result[1]['columns'][1]['val']);
@@ -254,7 +256,7 @@ class SearchRunWithCustomFieldTest extends CustomTestBase {
     $this->assertEquals('Married', $result[1]['columns'][2]['val']);
     $this->assertEquals($spouseRel, $result[1]['columns'][2]['edit']['record']['id']);
     $this->assertNull($result[1]['columns'][3]['val']);
-    // $this->assertArrayNotHasKey('edit', $result[1]['columns'][3]);
+    $this->assertArrayNotHasKey('edit', $result[1]['columns'][3]);
     $this->assertNull($result[1]['columns'][4]['val']);
     $this->assertEquals($spouseRel, $result[1]['columns'][4]['edit']['record']['id']);
 
@@ -269,4 +271,116 @@ class SearchRunWithCustomFieldTest extends CustomTestBase {
     $this->assertArrayNotHasKey('edit', $result[2]['columns'][4]);
   }
 
+  public function testEditableCustomFields() {
+    $subject = uniqid(__FUNCTION__);
+
+    $contact = Contact::create(FALSE)
+      ->execute()->single();
+
+    // CustomGroup based on Activity Type
+    CustomGroup::create(FALSE)
+      ->addValue('extends', 'Activity')
+      ->addValue('extends_entity_column_value:name', ['Meeting', 'Phone Call'])
+      ->addValue('title', 'meeting_phone')
+      ->addChain('field', CustomField::create()
+        ->addValue('custom_group_id', '$id')
+        ->addValue('label', 'sub_field')
+        ->addValue('html_type', 'Text')
+      )
+      ->execute();
+
+    $sampleData = [
+      ['activity_type_id:name' => 'Meeting', 'meeting_phone.sub_field' => 'Abc'],
+      ['activity_type_id:name' => 'Phone Call'],
+      ['activity_type_id:name' => 'Email'],
+    ];
+    $activity = $this->saveTestRecords('Activity', [
+      'defaults' => ['subject' => $subject, 'source_contact_id', $contact['id']],
+      'records' => $sampleData,
+    ]);
+
+    $activityTypes = array_column(
+      Activity::getFields(FALSE)->setLoadOptions(['id', 'name'])->addWhere('name', '=', 'activity_type_id')->execute()->single()['options'],
+      'id',
+      'name'
+    );
+
+    $params = [
+      'checkPermissions' => FALSE,
+      'return' => 'page:1',
+      'savedSearch' => [
+        'api_entity' => 'Activity',
+        'api_params' => [
+          'version' => 4,
+          'select' => ['subject', 'meeting_phone.sub_field'],
+          'where' => [['subject', '=', $subject]],
+        ],
+      ],
+      'display' => [
+        'type' => 'table',
+        'label' => '',
+        'settings' => [
+          'actions' => TRUE,
+          'pager' => [],
+          'columns' => [
+            [
+              'key' => 'subject',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'editable' => TRUE,
+            ],
+            [
+              'key' => 'meeting_phone.sub_field',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'editable' => TRUE,
+            ],
+          ],
+          'sort' => [
+            ['id', 'ASC'],
+          ],
+        ],
+      ],
+      'afform' => NULL,
+    ];
+
+    $result = civicrm_api4('SearchDisplay', 'run', $params);
+    // Custom field editable
+    $expectedCustomFieldEdit = [
+      'entity' => 'Activity',
+      'input_type' => 'Text',
+      'data_type' => 'String',
+      'options' => FALSE,
+      'serialize' => FALSE,
+      'nullable' => TRUE,
+      'fk_entity' => NULL,
+      'value_key' => 'meeting_phone.sub_field',
+      'record' => ['id' => $activity[0]['id']],
+      'action' => 'update',
+      'value' => 'Abc',
+    ];
+    $expectedSubjectEdit = ['value_key' => 'subject', 'value' => $subject] + $expectedCustomFieldEdit;
+
+    // First Activity
+    $this->assertEquals($expectedSubjectEdit, $result[0]['columns'][0]['edit']);
+    $this->assertEquals($expectedCustomFieldEdit, $result[0]['columns'][1]['edit']);
+    $this->assertEquals($activityTypes['Meeting'], $result[0]['data']['activity_type_id']);
+
+    // Second Activity
+    $expectedSubjectEdit['record']['id'] = $activity[1]['id'];
+    $expectedCustomFieldEdit['record']['id'] = $activity[1]['id'];
+    $expectedCustomFieldEdit['value'] = NULL;
+    $this->assertEquals($expectedSubjectEdit, $result[1]['columns'][0]['edit']);
+    $this->assertEquals($expectedCustomFieldEdit, $result[1]['columns'][1]['edit']);
+    $this->assertEquals($activityTypes['Phone Call'], $result[1]['data']['activity_type_id']);
+
+    // Third Activity
+    $expectedSubjectEdit['record']['id'] = $activity[2]['id'];
+    $this->assertEquals($expectedSubjectEdit, $result[2]['columns'][0]['edit']);
+    $this->assertTrue(!isset($result[2]['columns'][1]['edit']));
+    $this->assertEquals($activityTypes['Email'], $result[2]['data']['activity_type_id']);
+  }
+
 }
index 57ed34f9932a7d2f83a7c1573c10d20052dd7db5..4cb20cd03ee7b65c3fea57749294e07d8577166a 100644 (file)
     options: {
       url: null,
       block: true,
-      post: null,
       crmForm: null
     },
     _originalContent: null,
         return false;
       });
     },
-    _ajax: function(url) {
-      if (!this.options.post || !this.isOriginalUrl()) {
-        return $.getJSON(url);
-      }
-      return $.post({
-        url: url,
-        dataType: 'json',
-        data: this.options.post
-      });
-    },
     refresh: function() {
       var that = this;
       var url = this._formatUrl(this.options.url, 'json');
       if (this.options.crmForm) $('form', this.element).ajaxFormUnbind();
       if (this.options.block) this.element.block();
-      this._ajax(url).then(function(data) {
+      $.getJSON(url, function(data) {
         if (data.status === 'redirect') {
           that.options.url = data.userContext;
           return that.refresh();
             $('[name="'+formElement+'"]', that.element).crmError(msg);
           });
         }
-      }function(data, msg, status) {
+      }).fail(function(data, msg, status) {
         that._onFailure(data, status);
       });
     },
diff --git a/tests/phpunit/CRM/Contact/Import/Form/data/individual_locations_with_related.csv b/tests/phpunit/CRM/Contact/Import/Form/data/individual_locations_with_related.csv
new file mode 100644 (file)
index 0000000..e5dd31f
--- /dev/null
@@ -0,0 +1,2 @@
+First Name,Last Name,Birth Date,Street Address,City,Postal Code,Country,State,Email,Signature Text,IM,Website,Phone,Phone Ext,Mum’s first name,Last Name,Mum-Street Address,Mum-City,Mum-Country,Mum-State,Mum-email,Mum-signature Test,Mum-IM,Mum-website,Mum-phone,Mum-phone-ext,Mum-home-phone,Mum-home-mobile,Sister-Street Address,Sister-City,Sister-Country,Sister-State,Sister-email,Sister-signature Test,Sister-IM,Sister-website,Sister-phone,Sister-phone-ext,Team,Team Website,Team email ,Team Reference,Team Address1,Team Address 2,Team address Validity Date,Team Backup Website
+Susie,Jones,2002-01-08,24 Adelaide Road,Sydney,90210,Australia,NSW,susie@example.com,Regards,susiej,https://susie.example.com,999-4445,123,Mum,Jones,The Green House,,,,mum@example.com,,Mum-IM,http://mum.example.com,911,1,88-999,99-888,,,New Zealand,,sis@example.com,,,,555-666,,Soccer Superstars,https://super.example.org,tt@example.org,T-882,PO Box 999,Marion Square,2022-03-04,http://super-t.example.org
index c8cc450b60f1ddcd298c5bf9218a3d20d46ca2e5..57ce7109044206ec6cfa0f370becb1b0d6a2cbf5 100644 (file)
@@ -17,6 +17,7 @@
 use Civi\Api4\Address;
 use Civi\Api4\Contact;
 use Civi\Api4\ContactType;
+use Civi\Api4\LocationType;
 use Civi\Api4\RelationshipType;
 use Civi\Api4\UserJob;
 
@@ -1144,6 +1145,86 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     ];
   }
 
+  /**
+   * Test location importing, including for related contacts.
+   *
+   * @throws \CRM_Core_Exception
+   * @throws \API_Exception
+   */
+  public function testImportLocations(): void {
+    $csv = 'individual_locations_with_related.csv';
+    $relationships = (array) RelationshipType::get()->addSelect('name_a_b', 'id')->addWhere('name_a_b', 'IN', [
+      'Child of',
+      'Sibling of',
+      'Employee of',
+    ])->execute()->indexBy('name_a_b');
+
+    $childKey = $relationships['Child of']['id'] . '_a_b';
+    $siblingKey = $relationships['Sibling of']['id'] . '_a_b';
+    $employeeKey = $relationships['Employee of']['id'] . '_a_b';
+    $locations = LocationType::get()->execute()->indexBy('name');
+    $phoneTypeID = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Phone');
+    $mobileTypeID = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Mobile');
+    $skypeTypeID = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_IM', 'provider_id', 'Skype');
+    $mainWebsiteTypeID = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Website', 'website_type_id', 'Main');
+    $linkedInTypeID = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Website', 'website_type_id', 'LinkedIn');
+    $homeID = $locations['Home']['id'];
+    $workID = $locations['Work']['id'];
+    $mapper = [
+      ['first_name'],
+      ['last_name'],
+      ['birth_date'],
+      ['street_address', $homeID],
+      ['city', $homeID],
+      ['postal_code', $homeID],
+      ['country', $homeID],
+      ['state_province', $homeID],
+      // No location type ID means 'Primary'
+      ['email'],
+      ['signature_text'],
+      ['im', NULL, $skypeTypeID],
+      ['url', $mainWebsiteTypeID],
+      ['phone', $homeID, $phoneTypeID],
+      ['phone_ext', $homeID, $phoneTypeID],
+      [$childKey, 'first_name'],
+      [$childKey, 'last_name'],
+      [$childKey, 'street_address'],
+      [$childKey, 'city'],
+      [$childKey, 'country'],
+      [$childKey, 'state_province'],
+      [$childKey, 'email', $homeID],
+      [$childKey, 'signature_text', $homeID],
+      [$childKey, 'im', $homeID, $skypeTypeID],
+      [$childKey, 'url', $linkedInTypeID],
+      // Same location type, different phone typ in these phones
+      [$childKey, 'phone', $homeID, $phoneTypeID],
+      [$childKey, 'phone_ext', $homeID, $phoneTypeID],
+      [$childKey, 'phone', $homeID, $mobileTypeID],
+      [$childKey, 'phone_ext', $homeID, $mobileTypeID],
+      [$siblingKey, 'street_address', $homeID],
+      [$siblingKey, 'city', $homeID],
+      [$siblingKey, 'country', $homeID],
+      [$siblingKey, 'state_province', $homeID],
+      [$siblingKey, 'email', $homeID],
+      [$siblingKey, 'signature_text', $homeID],
+      [$childKey, 'im', $homeID, $skypeTypeID],
+      // The 2 is website_type_id (yes, small hard-coding cheat)
+      [$siblingKey, 'url', $linkedInTypeID],
+      [$siblingKey, 'phone', $workID, $phoneTypeID],
+      [$siblingKey, 'phone_ext', $workID, $phoneTypeID],
+      [$employeeKey, 'organization_name'],
+      [$employeeKey, 'url', $mainWebsiteTypeID],
+      [$employeeKey, 'email', $homeID],
+      [$employeeKey, 'do_not_import'],
+      [$employeeKey, 'street_address', $homeID],
+      [$employeeKey, 'supplemental_address_1', $homeID],
+      [$employeeKey, 'do_not_import'],
+      // Second website, different type.
+      [$employeeKey, 'url', $linkedInTypeID],
+    ];
+    $this->validateCSV($csv, $mapper);
+  }
+
   /**
    * Test that setting duplicate action to fill doesn't blow away data
    * that exists, but does fill in where it's empty.
@@ -1585,9 +1666,8 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
    *   Any submitted values overrides.
    *
    * @throws \API_Exception
-   * @throws \CRM_Core_Exception
    */
-  protected function validateCSV(string $csv, array $mapper, $submittedValues): void {
+  protected function validateCSV(string $csv, array $mapper, array $submittedValues = []): void {
     [$dataSource, $parser] = $this->getDataSourceAndParser($csv, $mapper, $submittedValues);
     $parser->validateValues(array_values($dataSource->getRow()));
   }
@@ -1612,6 +1692,7 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
       'mapper' => $mapper,
       'dataSource' => 'CRM_Import_DataSource_CSV',
       'file' => ['name' => $csv],
+      'dateFormats' => CRM_Core_Form_Date::DATE_yyyy_mm_dd,
       'onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE,
       'groups' => [],
     ], $submittedValues);
diff --git a/tests/phpunit/CRM/Contribute/Import/Parser/data/.~lock.contributions.csv# b/tests/phpunit/CRM/Contribute/Import/Parser/data/.~lock.contributions.csv#
deleted file mode 100644 (file)
index 323cbd8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-,eileen,eileen-laptop,20.05.2022 17:00,file:///home/eileen/.config/libreoffice/4;
\ No newline at end of file
index 599f2ee4bb69a472e6ad3cd9cb999fd0f623f6c7..39377fa24587fc99acc88a3d37f87d52f4f32645 100644 (file)
@@ -2523,6 +2523,47 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
     $this->assertEquals($contributionRecur['values'][1]['is_test'], $repeatedContribution['values'][2]['is_test']);
   }
 
+  /**
+   * Test repeat contribution accepts recur_id instead of
+   * original_contribution_id.
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function testRepeatTransactionPreviousContributionRefunded(): void {
+    $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', [
+      'contact_id' => $this->_individualId,
+      'installments' => '12',
+      'frequency_interval' => '1',
+      'amount' => '100',
+      'contribution_status_id' => 1,
+      'start_date' => '2012-01-01 00:00:00',
+      'currency' => 'USD',
+      'frequency_unit' => 'month',
+      'payment_processor_id' => $this->paymentProcessorID,
+    ]);
+    $this->callAPISuccess('contribution', 'create', array_merge(
+        $this->_params,
+        [
+          'contribution_recur_id' => $contributionRecur['id'],
+          'contribution_status_id' => 'Refunded',
+        ]
+      )
+    );
+
+    $this->callAPISuccess('contribution', 'repeattransaction', [
+      'contribution_recur_id' => $contributionRecur['id'],
+      'trxn_id' => 1234,
+    ]);
+    $contributions = $this->callAPISuccess('contribution', 'get', [
+      'contribution_recur_id' => $contributionRecur['id'],
+      'sequential' => 1,
+    ]);
+    // We should have contribution 0 in "Refunded" status and contribution 1 in "Pending" status
+    $this->assertEquals(2, $contributions['count']);
+    $this->assertEquals(7, $contributions['values'][0]['contribution_status_id']);
+    $this->assertEquals(2, $contributions['values'][1]['contribution_status_id']);
+  }
+
   /**
    * CRM-19945 Tests that Contribute.repeattransaction renews a membership when contribution status=Completed
    *
index b1cd25753ace1ce54ed464307a2ea5c9bbe6d160..88be40b997d068b3ffcfd33905fecca52dcda277 100644 (file)
     <name>is_show_location</name>
     <type>boolean</type>
     <required>true</required>
-    <title>show location</title>
+    <title>Show Location</title>
     <default>1</default>
     <comment>If true, show event location.</comment>
     <add>1.7</add>