From 57d569063180598658764f4f734b8bbc7a37c274 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Sun, 29 May 2022 22:57:52 +1200 Subject: [PATCH] Fix state & country handling in contact import Fixes a regression where case has become sensitive. Also fixes custom state & country fields to be case insensitive --- CRM/Contact/BAO/Contact.php | 40 --- CRM/Contact/Import/Parser/Contact.php | 330 +++++++----------- CRM/Import/Parser.php | 91 ++++- ...dual_country_state_county_with_related.csv | 2 + tests/phpunit/api/v3/ContactTest.php | 1 - 5 files changed, 219 insertions(+), 245 deletions(-) diff --git a/CRM/Contact/BAO/Contact.php b/CRM/Contact/BAO/Contact.php index 3c10894a6a9a..b808d6ed05e2 100644 --- a/CRM/Contact/BAO/Contact.php +++ b/CRM/Contact/BAO/Contact.php @@ -781,46 +781,6 @@ protected static function contactTrash($contact): bool { * */ public static function resolveDefaults(&$defaults, $reverse = FALSE) { - - $blocks = ['address']; - foreach ($blocks as $name) { - if (!array_key_exists($name, $defaults) || !is_array($defaults[$name])) { - continue; - } - foreach ($defaults[$name] as $count => & $values) { - - //get location type id. - CRM_Utils_Array::lookupValue($values, 'location_type', CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id'), $reverse); - - if ($name == 'address') { - // FIXME: lookupValue doesn't work for vcard_name - if (!empty($values['location_type_id'])) { - $vcardNames = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id', ['labelColumn' => 'vcard_name']); - $values['vcard_name'] = $vcardNames[$values['location_type_id']]; - } - - $stateProvinceID = self::resolveStateProvinceID($values, $values['country_id'] ?? NULL); - if ($stateProvinceID) { - $values['state_province_id'] = $stateProvinceID; - } - - if (!empty($values['state_province_id'])) { - $countyList = CRM_Core_PseudoConstant::countyForState($values['state_province_id']); - } - else { - $countyList = CRM_Core_PseudoConstant::county(); - } - CRM_Utils_Array::lookupValue($values, - 'county', - $countyList, - $reverse - ); - } - - // Kill the reference. - unset($values); - } - } } /** diff --git a/CRM/Contact/Import/Parser/Contact.php b/CRM/Contact/Import/Parser/Contact.php index 11f2fa4ab78c..6b5e897f416c 100644 --- a/CRM/Contact/Import/Parser/Contact.php +++ b/CRM/Contact/Import/Parser/Contact.php @@ -11,6 +11,7 @@ use Civi\Api4\Contact; use Civi\Api4\RelationshipType; +use Civi\Api4\StateProvince; require_once 'api/v3/utils.php'; @@ -640,40 +641,16 @@ private static function legacyCreateMultiple($params, $ids = []) { * Contain record values. * @param array $formatted * Array of formatted data. - * @param array $contactFields - * Contact DAO fields. */ - private function formatCommonData($params, &$formatted, $contactFields) { + private function formatCommonData($params, &$formatted) { $customFields = CRM_Core_BAO_CustomField::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ?? NULL); $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address'); $customFields = $customFields + $addressCustomFields; //format date first - $session = CRM_Core_Session::singleton(); - $dateType = $session->get("dateTypes"); - foreach ($params as $key => $val) { - $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key); - if ($customFieldID && - !array_key_exists($customFieldID, $addressCustomFields) - ) { - //we should not update Date to null, CRM-4062 - if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) { - //CRM-21267 - CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key); - } - elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') { - if (empty($val) && !is_numeric($val) && $this->_onDuplicate == CRM_Import_Parser::DUPLICATE_FILL) { - //retain earlier value when Import mode is `Fill` - unset($params[$key]); - } - else { - $params[$key] = CRM_Utils_String::strtoboolstr($val); - } - } - } - } - $metadataBlocks = ['phone', 'im', 'openid', 'email']; + + $metadataBlocks = ['phone', 'im', 'openid', 'email', 'address']; foreach ($metadataBlocks as $block) { foreach ($formatted[$block] ?? [] as $blockKey => $blockValues) { if ($blockValues['location_type_id'] === 'Primary') { @@ -728,51 +705,6 @@ private function formatCommonData($params, &$formatted, $contactFields) { $formatted[$key] = $field; } $this->formatContactParameters($formatValues, $formatted); - - //Handling Custom Data - // note: Address custom fields will be handled separately inside formatContactParameters - if (($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && - array_key_exists($customFieldID, $customFields) && - !array_key_exists($customFieldID, $addressCustomFields) - ) { - - $extends = $customFields[$customFieldID]['extends'] ?? NULL; - $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL; - $dataType = $customFields[$customFieldID]['data_type'] ?? NULL; - $serialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]); - - if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) { - $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE); - foreach ($customOption as $customValue) { - $val = $customValue['value'] ?? NULL; - $label = strtolower($customValue['label'] ?? ''); - $value = strtolower(trim($formatted[$key])); - if (($value == $label) || ($value == strtolower($val))) { - $params[$key] = $formatted[$key] = $val; - } - } - } - elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) { - $mulValues = explode(',', $formatted[$key]); - $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE); - $formatted[$key] = []; - $params[$key] = []; - foreach ($mulValues as $v1) { - foreach ($customOption as $v2) { - if ((strtolower($v2['label']) == strtolower(trim($v1))) || - (strtolower($v2['value']) == strtolower(trim($v1))) - ) { - if ($htmlType == 'CheckBox') { - $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1; - } - else { - $params[$key][] = $formatted[$key][] = $v2['value']; - } - } - } - } - } - } } if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && array_key_exists($customFieldID, $customFields) && @@ -875,18 +807,10 @@ public static function isErrorInCustomData($params, &$errorMessage, $csType = NU $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType); } - $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address'); $parser = new CRM_Contact_Import_Parser_Contact(); foreach ($params as $key => $value) { if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) { - //For address custom fields, we do get actual custom field value as an inner array of - //values so need to modify - if (array_key_exists($customFieldID, $addressCustomFields)) { - $locationTypeID = array_key_first($value); - $value = $value[$locationTypeID][$key]; - $errors[] = $parser->validateCustomField($customFieldID, $value, $addressCustomFields[$customFieldID], $dateType); - } - else { + if (1) { if (!array_key_exists($customFieldID, $customFields)) { return ts('field ID'); } @@ -894,24 +818,6 @@ public static function isErrorInCustomData($params, &$errorMessage, $csType = NU $errors[] = $parser->validateCustomField($customFieldID, $value, $customFields[$customFieldID], $dateType); } } - elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) { - //CRM-5125 - //supporting custom data of related contact subtypes - $relation = $key; - if (!empty($relation)) { - [$id, $first, $second] = CRM_Utils_System::explode('_', $relation, 3); - $direction = "contact_sub_type_$second"; - $relationshipType = new CRM_Contact_BAO_RelationshipType(); - $relationshipType->id = $id; - if ($relationshipType->find(TRUE)) { - if (isset($relationshipType->$direction)) { - $params[$key]['contact_sub_type'] = $relationshipType->$direction; - } - } - } - - self::isErrorInCustomData($params[$key], $errorMessage, $csType); - } } if ($errors) { $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', array_filter($errors)); @@ -927,45 +833,11 @@ public static function isErrorInCustomData($params, &$errorMessage, $csType = NU */ public function isErrorInCoreData($params, &$errorMessage) { $errors = []; - if (!empty($params['contact_sub_type']) && !CRM_Contact_BAO_ContactType::isExtendsContactType($params['contact_sub_type'], $params['contact_type'])) { - $errors[] = ts('Mismatched or Invalid Contact Subtype.'); - } foreach ($params as $key => $value) { if ($value) { switch ($key) { - - case 'state_province': - if (!empty($value)) { - foreach ($value as $stateValue) { - if ($stateValue['state_province']) { - if (self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvinceAbbreviation()) || - self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvince()) - ) { - continue; - } - else { - $errors[] = ts('State/Province'); - } - } - } - } - break; - - case 'county': - if (!empty($value)) { - foreach ($value as $county) { - if ($county['county']) { - $countyNames = CRM_Core_PseudoConstant::county(); - if (!empty($county['county']) && !in_array($county['county'], $countyNames)) { - $errors[] = ts('County input value not in county table: The County value appears to be invalid. It does not match any value in CiviCRM table of counties.'); - } - } - } - } - break; - case 'do_not_email': case 'do_not_phone': case 'do_not_mail': @@ -1783,6 +1655,7 @@ public function run( $this->_rowCount++; $this->_totalCount++; + $returnCode = NULL; if ($mode == self::MODE_PREVIEW) { $returnCode = $this->preview($values); @@ -1873,6 +1746,8 @@ private function getParams(array $values): array { } } + $this->fillStateProvince($params); + return $params; } @@ -2010,7 +1885,6 @@ protected function formatContactParameters(&$values, &$params) { // Cache the various object fields // @todo - remove this after confirming this is just a compilation of other-wise-cached fields. static $fields = []; - if (isset($values['note'])) { // add a note field if (!isset($params['note'])) { @@ -2036,21 +1910,11 @@ protected function formatContactParameters(&$values, &$params) { return TRUE; } - // Check for custom field values - $customFields = CRM_Core_BAO_CustomField::getFields(CRM_Utils_Array::value('contact_type', $values), - FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE - ); - foreach ($values as $key => $value) { if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) { // check if it's a valid custom field id - if (!array_key_exists($customFieldID, $customFields)) { - return civicrm_api3_create_error('Invalid custom field ID'); - } - else { - $params[$key] = $value; - } + $params[$key] = $value; } } return TRUE; @@ -2069,12 +1933,6 @@ protected function formatContactParameters(&$values, &$params) { * @throws \CiviCRM_API3_Exception */ protected function formatLocationBlock(&$values, &$params) { - - // handle address fields. - if (!array_key_exists('address', $params) || !is_array($params['address'])) { - $params['address'] = []; - } - // Note: we doing multiple value formatting here for address custom fields, plus putting into right format. // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving // the address in CRM_Core_BAO_Address::create method @@ -2115,33 +1973,6 @@ protected function formatLocationBlock(&$values, &$params) { $values = $newValues; } - $fields['Address'] = $this->getMetadataForEntity('Address'); - // @todo this is kinda replicated below.... - _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$values['location_type_id']]); - - $addressFields = [ - 'county', - 'country_id', - 'state_province', - 'supplemental_address_1', - 'supplemental_address_2', - 'supplemental_address_3', - 'StateProvince.name', - ]; - foreach (array_keys($customFields) as $customFieldID) { - $addressFields[] = 'custom_' . $customFieldID; - } - - foreach ($addressFields as $field) { - if (array_key_exists($field, $values)) { - if (!array_key_exists('address', $params)) { - $params['address'] = []; - } - $params['address'][$values['location_type_id']][$field] = $values[$field]; - } - } - - $this->fillPrimary($params['address'][$values['location_type_id']], $values, 'address', CRM_Utils_Array::value('id', $params)); return TRUE; } @@ -2341,9 +2172,12 @@ public function validateValues(array $values): void { if (!empty($value['relationship_type_id'])) { $requiredSubType = $this->getRelatedContactSubType($value['relationship_type_id'], $value['relationship_direction']); if ($requiredSubType && $value['contact_sub_type'] && $requiredSubType !== $value['contact_sub_type']) { - throw new CRM_Core_Exception($prefixString . ts('Mismatched or Invalid contact subtype found for this related contact.')); + $errors[] = $prefixString . ts('Mismatched or Invalid contact subtype found for this related contact.'); } } + if (!empty($value['contact_sub_type']) && !CRM_Contact_BAO_ContactType::isExtendsContactType($value['contact_sub_type'], $value['contact_type'])) { + $errors[] = ts('Mismatched or Invalid Contact Subtype.'); + } } //check for duplicate external Identifier @@ -2362,14 +2196,11 @@ public function validateValues(array $values): void { //date-format part ends $errorMessage = implode(', ', $errors); - //checking error in custom data - $this->isErrorInCustomData($params, $errorMessage, $params['contact_sub_type'] ?? NULL); //checking error in core data $this->isErrorInCoreData($params, $errorMessage); if ($errorMessage) { - $tempMsg = "Invalid value for field(s) : $errorMessage"; - throw new CRM_Core_Exception($tempMsg); + throw new CRM_Core_Exception("Invalid value for field(s) : $errorMessage"); } } @@ -2506,7 +2337,7 @@ protected function getRelationshipType(int $relationshipTypeID): array { */ private function addFieldToParams(array &$contactArray, array $locationValues, string $fieldName, $importedValue): void { if (!empty($locationValues)) { - $fieldMap = ['country' => 'country_id']; + $fieldMap = ['country' => 'country_id', 'state_province' => 'state_province_id', 'county' => 'county_id']; $realFieldName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName]; $entity = strtolower($this->getFieldEntity($fieldName)); @@ -2526,24 +2357,20 @@ private function addFieldToParams(array &$contactArray, array $locationValues, s } } - // The new way... + if (!empty($fieldValue) && $realFieldName === 'state_province_id' && is_numeric($fieldValue)) { + if ($this->getAvailableStates() && empty($this->getAvailableStates()[$fieldValue])) { + // We restrict to allowed states for address fields - if we are not yet resolved to a number + // then we must skip here. + $fieldValue = 'invalid_import_value'; + } + } + if (!isset($contactArray[$entity][$entityKey])) { $contactArray[$entity][$entityKey] = $locationValues; } - // Honestly I'll explain in comment_final_version(revision_2)_use_this_one... - $reallyRealFieldName = $fieldName === 'im' ? 'name' : $fieldName; + // So im has really non-standard handling... + $reallyRealFieldName = $realFieldName === 'im' ? 'name' : $realFieldName; $contactArray[$entity][$entityKey][$reallyRealFieldName] = $fieldValue; - - if (!isset($locationValues[$fieldName]) && $entity === 'address') { - // These lines add the values to params 'the old way' - // The old way is then re-formatted by formatCommonData more - // or less as per below. - // @todo - stop doing this & remove handling in formatCommonData. - $locationValues[$fieldName] = $fieldValue; - $contactArray[$fieldName] = (array) ($contactArray[$fieldName] ?? []); - $contactArray[$fieldName][$entityKey] = $locationValues; - $contactArray[$entity][$entityKey][$realFieldName] = $fieldValue; - } } else { $fieldName = array_search($fieldName, $this->getOddlyMappedMetadataFields(), TRUE) ?: $fieldName; @@ -2696,4 +2523,111 @@ protected function processContact(array $params, array $formatted): array { return array($formatted, $params); } + /** + * Try to get the correct state province using what country information we have. + * + * If the state matches more than one possibility then either the imported + * country of the site country should help us.... + * + * @param string $stateProvince + * @param int|null|string $countryID + * + * @return int|string + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + private function tryToResolveStateProvince(string $stateProvince, $countryID) { + // Try to disambiguate since we likely have the country now. + $possibleStates = $this->ambiguousOptions['state_province_id'][mb_strtolower($stateProvince)]; + if ($countryID) { + return $this->checkStatesForCountry($countryID, $possibleStates) ?: 'invalid_import_value'; + } + // Try the default country next. + $defaultCountryMatch = $this->checkStatesForCountry($this->getSiteDefaultCountry(), $possibleStates); + if ($defaultCountryMatch) { + return $defaultCountryMatch; + } + + if ($this->getAvailableCountries()) { + $countryMatches = []; + foreach ($this->getAvailableCountries() as $availableCountryID) { + $possible = $this->checkStatesForCountry($availableCountryID, $possibleStates); + if ($possible) { + $countryMatches[] = $possible; + } + } + if (count($countryMatches) === 1) { + return reset($countryMatches); + } + + } + return $stateProvince; + } + + /** + * @param array $params + * + * @return array + * @throws \API_Exception + */ + private function fillStateProvince(array &$params): array { + foreach ($params as $key => $value) { + if ($key === 'address') { + foreach ($value as $index => $address) { + $stateProvinceID = $address['state_province_id'] ?? NULL; + if ($stateProvinceID) { + if (!is_numeric($stateProvinceID)) { + $params['address'][$index]['state_province_id'] = $this->tryToResolveStateProvince($stateProvinceID, $address['country_id'] ?? NULL); + } + elseif (!empty($address['country_id']) && is_numeric($address['country_id'])) { + if (!$this->checkStatesForCountry((int) $address['country_id'], [$stateProvinceID])) { + $params['address'][$index]['state_province_id'] = 'invalid_import_value'; + } + } + } + } + } + elseif (is_array($value) && !in_array($key, ['email', 'phone', 'im', 'website', 'openid'], TRUE)) { + $this->fillStateProvince($params[$key]); + } + } + return $params; + } + + /** + * Check is any of the given states correlate to the country. + * + * @param int $countryID + * @param array $possibleStates + * + * @return int|null + * @throws \API_Exception + */ + private function checkStatesForCountry(int $countryID, array $possibleStates) { + foreach ($possibleStates as $index => $state) { + if (!empty($this->statesByCountry[$state])) { + if ($this->statesByCountry[$state] === $countryID) { + return $state; + } + unset($possibleStates[$index]); + } + } + if (!empty($possibleStates)) { + $states = StateProvince::get(FALSE) + ->addSelect('country_id') + ->addWhere('id', 'IN', $possibleStates) + ->execute() + ->indexBy('country_id'); + foreach ($states as $state) { + $this->statesByCountry[$state['id']] = $state['country_id']; + } + foreach ($possibleStates as $state) { + if ($this->statesByCountry[$state] === $countryID) { + return $state; + } + } + } + return FALSE; + } + } diff --git a/CRM/Import/Parser.php b/CRM/Import/Parser.php index cd05171b4911..5f773df9b58b 100644 --- a/CRM/Import/Parser.php +++ b/CRM/Import/Parser.php @@ -65,6 +65,23 @@ abstract class CRM_Import_Parser { */ protected $metadataHandledFields = []; + /** + * Potentially ambiguous options. + * + * For example 'UT' is a state in more than one country. + * + * @var array + */ + protected $ambiguousOptions = []; + + + /** + * States to country mapping. + * + * @var array + */ + protected $statesByCountry = []; + /** * @return int|null */ @@ -91,6 +108,18 @@ public function setUserJobID(int $userJobID): self { */ private $availableCountries; + /** + * States that the site is restricted to + * + * @var array|false + */ + private $availableStates; + + /** + * @var int + */ + private $siteDefaultCountry; + /** * Get User Job. * @@ -1165,9 +1194,7 @@ protected function validateRequiredFields(array $requiredFields, array $params, * @throws \API_Exception */ protected function getTransformedFieldValue(string $fieldName, $importedValue) { - $transformableFields = array_merge($this->metadataHandledFields, ['country_id']); - // For now only do gender_id etc as we need to work through removing duplicate handling - if (empty($importedValue) || !in_array($fieldName, $transformableFields, TRUE)) { + if (empty($importedValue)) { return $importedValue; } $fieldMetadata = $this->getFieldMetadata($fieldName); @@ -1202,6 +1229,12 @@ protected function getTransformedFieldValue(string $fieldName, $importedValue) { } $options = $this->getFieldOptions($fieldName); if ($options !== FALSE) { + if ($this->isAmbiguous($fieldName, $importedValue)) { + // We can't transform it at this stage. Perhaps later we can with + // other information such as country. + return $importedValue; + } + $comparisonValue = is_numeric($importedValue) ? $importedValue : mb_strtolower($importedValue); return $options[$comparisonValue] ?? 'invalid_import_value'; } @@ -1233,7 +1266,7 @@ protected function getFieldOptions(string $fieldName) { * @throws \API_Exception * @throws \Civi\API\Exception\NotImplementedException */ - protected function getFieldMetadata(string $fieldName, bool $loadOptions = FALSE, $limitToContactType = FALSE): array { + protected function getFieldMetadata(string $fieldName, bool $loadOptions = FALSE, bool $limitToContactType = FALSE): array { $fieldMap = $this->getOddlyMappedMetadataFields(); $fieldMapName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName]; @@ -1268,10 +1301,17 @@ protected function getFieldMetadata(string $fieldName, bool $loadOptions = FALSE foreach (['name', 'label', 'abbr'] as $key) { $optionValue = mb_strtolower($option[$key] ?? ''); if ($optionValue !== '') { - $values[$optionValue] = $option['id']; + if (isset($values[$optionValue]) && $values[$optionValue] !== $option['id']) { + if (!isset($this->ambiguousOptions[$fieldName][$optionValue])) { + $this->ambiguousOptions[$fieldName][$optionValue] = [$values[$optionValue]]; + } + $this->ambiguousOptions[$fieldName][$optionValue][] = $option['id']; + } + else { + $values[$optionValue] = $option['id']; + } } } - } $this->importableFieldsMetadata[$fieldMapName]['options'] = $values; } @@ -1447,4 +1487,43 @@ protected function getOddlyMappedMetadataFields(): array { ]; } + /** + * Get the default country for the site. + * + * @return int + */ + protected function getSiteDefaultCountry(): int { + if (!isset($this->siteDefaultCountry)) { + $this->siteDefaultCountry = (int) Civi::settings()->get('defaultContactCountry'); + } + return $this->siteDefaultCountry; + } + + /** + * Get the available countries. + * + * If the site is not configured with a restriction then all countries are valid + * but otherwise only a select array are. + * + * @return array|false + * FALSE indicates no restrictions. + */ + protected function getAvailableStates() { + if ($this->availableStates === NULL) { + $availableStates = Civi::settings()->get('provinceLimit'); + $this->availableStates = !empty($availableStates) ? array_fill_keys($availableStates, TRUE) : FALSE; + } + return $this->availableStates; + } + + /** + * Is the option ambigious. + * + * @param string $fieldName + * @param string $importedValue + */ + protected function isAmbiguous(string $fieldName, $importedValue): bool { + return !empty($this->ambiguousOptions[$fieldName][mb_strtolower($importedValue)]); + } + } diff --git a/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv b/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv index ef3cf1961fa7..cf7fc1a2d913 100644 --- a/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv +++ b/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv @@ -5,3 +5,5 @@ Susie,Jones,susie@example.com,,Australia,NSW,NSW,Australia,Australia,NSW,Mum,Jon Susie,Jones,susie@example.com,,AU,New South Wales,New South Wales,AU,AU,New South Wales,Mum,Jones,mum@example.com,New South Wales,AU,,AU,New South Wales,Australia,New South Wales,Valid, Susie,Jones,susie@example.com,,1013,New South Wales,,1013,1013,New South Wales,Mum,Jones,mum@example.com,New South Wales,1013,,1013,New South Wales,1013,New South Wales,Valid, Susie,Jones,susie@example.com,,AUSTRALIA,,,,,,Mum,Jones,mum@example.com,,austRalia,,,,,,Valid, +Susie,Jones,susie@example.com,,AU,NEW South Wales,NEW South Wales,AU,AU,NEW South Wales,Mum,Jones,mum@example.com,NEW South Wales,AU,,AU,NEW South Wales,Australia,NEW South Wales,Valid, +Susie,Jones,susie@example.com,,NZ,NEW South Wales,,,,,Mum,Jones,mum@example.com,New South Wales,AU,,AU,New South Wales,Australia,New South Wales,Invalid,Wrong country+state diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php index 673527b70d9d..27c5ea68a2a1 100644 --- a/tests/phpunit/api/v3/ContactTest.php +++ b/tests/phpunit/api/v3/ContactTest.php @@ -2305,7 +2305,6 @@ public function testContactGetEmail(): void { * * @param int $version * - * @throws \CRM_Core_Exception * @dataProvider versionThreeAndFour */ public function testSetPreferredCommunicationNull(int $version): void {