diff --git a/CRM/Streetimport/GP/Handler/GPRecordHandler.php b/CRM/Streetimport/GP/Handler/GPRecordHandler.php index 35f702c..6b8d091 100644 --- a/CRM/Streetimport/GP/Handler/GPRecordHandler.php +++ b/CRM/Streetimport/GP/Handler/GPRecordHandler.php @@ -6,6 +6,8 @@ | http://www.systopia.de/ | +--------------------------------------------------------------*/ +use Civi\Api4; + /** * Abstract class bundle common GP importer functions * @@ -559,7 +561,7 @@ public function isContributionRecurActive($contribution_recur) { * - else: if new data wouldn't replace ALL the data of the old address -> create ticket (activity) for manual processing * - else: update address */ - public function createOrUpdateAddress($contact_id, $address_data, $record) { + public function createOrUpdateAddress($contact_id, $address_data, $record, $parent_activity_id = NULL) { if (empty($address_data)) return; // check if address is complete @@ -1138,4 +1140,89 @@ protected function isDeletedContact($contact_id) { ]) == 1; } + /** + * Check if there has been a change since a specified date + * + * @param $contact_id + * @param $minimum_date + * @param $record + * + * @return bool + */ + public function addressChangeRecordedSince($contact_id, $minimum_date, $record) { + $logging = new CRM_Logging_Schema(); + + // Assert that logging is enabled + if (!$logging->isEnabled()) { + $this->logger->logDebug("Logging not enabled, cannot determine whether records have changed.", $record); + return FALSE; + } + + // Determine the name of the logging database + $dsn_database = ( + defined('CIVICRM_LOGGING_DSN') + ? DB::parseDSN(CIVICRM_LOGGING_DSN) + : DB::parseDSN(CIVICRM_DSN) + )['database']; + + // The following address attributes will be used for comparison + $relevant_attributes = [ + 'city', + 'country_id', + 'is_primary', + 'postal_code', + 'street_address', + 'supplemental_address_1', + 'supplemental_address_2', + ]; + + $attribute_list = implode(', ', $relevant_attributes); + + // Determine the primary address of the contact at the time of $minimum_date + $prev_addr_query = CRM_Core_DAO::executeQuery(" + SELECT $attribute_list + FROM $dsn_database.log_civicrm_address + WHERE + contact_id = $contact_id + AND is_primary = 1 + AND log_action != 'Delete' + AND log_date < '$minimum_date' + ORDER BY log_date DESC + LIMIT 1 + "); + + if (!$prev_addr_query->fetch()) return TRUE; + + // Discard DB query metadata, keep only relevant attributes + $previous_address = CRM_Streetimport_Utils::selectKeys( + (array) (clone $prev_addr_query), + $relevant_attributes + ); + + // Get the current primary address of the contact + $current_address = Api4\Address::get(FALSE) + ->addSelect(...$relevant_attributes) + ->addWhere('contact_id', '=', $contact_id) + ->addWhere('is_primary', '=', TRUE) + ->setLimit(1) + ->execute() + ->first(); + + if (is_null($current_address)) { + $this->logger->logDebug("Contact #$contact_id has no current primary address", $record); + return TRUE; + } + + foreach ($relevant_attributes as $attribute) { + if ($previous_address[$attribute] == $current_address[$attribute]) continue; + + // A relevant attribute has changed + $this->logger->logDebug("Address attribute '$attribute' changed", $record); + return TRUE; + } + + // No relevant changes have been detected + return FALSE; + } + } diff --git a/CRM/Streetimport/GP/Handler/PostalReturn/Base.php b/CRM/Streetimport/GP/Handler/PostalReturn/Base.php index 2d9bc94..7d06a20 100644 --- a/CRM/Streetimport/GP/Handler/PostalReturn/Base.php +++ b/CRM/Streetimport/GP/Handler/PostalReturn/Base.php @@ -162,71 +162,6 @@ protected function findLastRTS($contact_id, $record, $search_frame = NULL, $cate */ abstract protected function getCategory($record); - /** - * Check if there has been a change since - * - * @param $contact_id - * @param $minimum_date - * @param $record - * - * @return bool - */ - protected function addressChangeRecordedSince($contact_id, $minimum_date, $record) { - // check if logging is enabled - $logging = new CRM_Logging_Schema(); - if (!$logging->isEnabled()) { - $this->logger->logDebug("Logging not enabled, cannot determine whether records have changed.", $record); - return FALSE; - } - - // query the logging DB - $dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN); - $relevant_attributes = array('id','is_primary','street_address','supplemental_address_1','supplemental_address_2','city','postal_code','country_id','log_date'); - $attribute_list = implode(',', $relevant_attributes); - $current_status = array(); - $query = CRM_Core_DAO::executeQuery("SELECT {$attribute_list} FROM {$dsn['database']}.log_civicrm_address WHERE contact_id={$contact_id}"); - while ($query->fetch()) { - // generate record - $record = array(); - $record_id = $query->id; - foreach ($relevant_attributes as $attribute) { - $record[$attribute] = $query->$attribute; - } - - // process record - if (!isset($current_status[$record_id])) { - // this is a new address - $current_status[$record_id] = $record; - - } else { - // compare with the old record - $old_record = $current_status[$record_id]; - $changed = FALSE; - foreach ($relevant_attributes as $attribute) { - if ($attribute == 'log_date') continue; // that doesn't matter - if ($old_record[$attribute] != $record[$attribute]) { - $this->logger->logDebug("Address attribute '{$attribute}' changed (on {$record['log_date']})", $record); - $changed = TRUE; - break; - } - } - - // this is the new current - $current_status[$record_id] = $record; - - if ($changed) { - // there is a change, check if it's in the time frame we're looking for - if (strtotime($record['log_date']) >= strtotime($minimum_date)) { - $this->logger->logDebug("Address change relevant (date range)", $record); - return TRUE; - } - } - } - } - - return FALSE; - } - /** * Get the correct subject for the activity * @@ -401,20 +336,26 @@ protected function addressChanged(array $originalAddress, array $returnAddress) * @throws \CiviCRM_API3_Exception */ protected function processReturn($record) { - $contact_id = $this->getContactID($record); + $contact_id = (int) $this->getContactID($record); + $campaign_id = (int) $this->getCampaignID($record); $category = $this->getCategory($record); $primary_address = $this->getPrimaryAddress($contact_id, $record); // whether to increase RTS counter where appropriate $increaseCounter = TRUE; // find parent activity - $parent_activity = $this->getParentActivity( - (int) $this->getContactID($record), - $this->getCampaignID($record), - [ - 'media' => ['letter_mail'], - 'exclude_activity_types' => ['Response'], - ] - ); + $parent_activity = $this->getParentActivity($contact_id, $campaign_id, [ + 'exclude_activity_types' => ['Response'], + 'media' => ['letter_mail'], + ]); + + // Also include webshop orders in the search + if (empty($parent_activity)) { + $parent_activity = $this->getParentActivity($contact_id, $campaign_id, [ + 'activity_types' => ['Webshop Order'], + 'media' => ['back_office', 'web'], + ]); + } + if (empty($parent_activity)) { // no parent activity found, continue with last RTS activity $parent_activity = $this->findLastRTS($contact_id, $record); @@ -425,7 +366,7 @@ protected function processReturn($record) { $increaseCounter = FALSE; } } - elseif (!empty($parent_activity) && $this->addressChangeRecordedSince($contact_id, $parent_activity['activity_date_time'], $record)) { + elseif (!empty($parent_activity) && $this->addressChangeRecordedSince($contact_id, $parent_activity['created_date'], $record)) { $this->logger->logDebug("Skipping RTS increase due to logged address change for contact [{$contact_id}].", $record); $increaseCounter = FALSE; } diff --git a/CRM/Streetimport/GP/Handler/TEDIContactRecordHandler.php b/CRM/Streetimport/GP/Handler/TEDIContactRecordHandler.php index cd9600f..e8e520c 100644 --- a/CRM/Streetimport/GP/Handler/TEDIContactRecordHandler.php +++ b/CRM/Streetimport/GP/Handler/TEDIContactRecordHandler.php @@ -6,6 +6,7 @@ | http://www.systopia.de/ | +--------------------------------------------------------------*/ +use Civi\Api4; use Civi\Api4\Contact; /** @@ -56,6 +57,14 @@ public function processRecord($record, $sourceURI) { } } + // Determine the parent activity ID + $parent_id = $this->getActivityId($record); + + if (empty($parent_id)) { + $this->logger->logImport($record, FALSE, $config->translate('TM Contact')); + return $this->logger->logError('Could not find parent activity', $record); + } + // TODO: remove this workaround once TEDI stops sending files with full country names if (!empty($record['Land']) && strlen($record['Land']) != 2) { $record['Land'] = ''; @@ -65,7 +74,7 @@ public function processRecord($record, $sourceURI) { if ($this->isContactReachedResponse($record['Ergebnisnummer']) || $this->isTrue($record, 'gespraech_stattgefunden')) { // apply contact base data updates if provided // FIELDS: nachname vorname firma TitelAkademisch TitelAdel TitelAmt Anrede geburtsdatum geburtsjahr strasse hausnummer hausnummernzusatz Land PLZ Ort email - $this->performContactBaseUpdates($contact_id, $record); + $this->performContactBaseUpdates($contact_id, $record, $parent_id); // Sign up for newsletter // FIELDS: emailNewsletter if ($this->isTrue($record, 'emailNewsletter')) { @@ -152,12 +161,6 @@ public function processRecord($record, $sourceURI) { return $this->logger->logError("Invalid Country for Contact [{$record['id']}]: '{$record['Land']}'", $record); } - $parent_id = $this->getActivityId($record); - if (empty($parent_id)) { - $this->logger->logImport($record, FALSE, $config->translate('TM Contact')); - return $this->logger->logError('Could not find parent activity', $record); - } - $createResponse = TRUE; /************************************ @@ -350,17 +353,22 @@ protected function getDate($record) { * * @param $contact_id * @param $record + * @param $parent_activity_id * * @throws \CiviCRM_API3_Exception */ - public function performContactBaseUpdates($contact_id, $record) { + public function performContactBaseUpdates($contact_id, $record, $parent_activity_id) { //Contact Entity $contact_params = $this->getPreparedContactParams($record); $this->updateContact($contact_params, $contact_id, $record); - //Address Entity - $address_params = $this->getPreparedAddressParams($record); - $this->createOrUpdateAddress($contact_id, $address_params, $record); + // Address Entity + $this->createOrUpdateAddress( + $contact_id, + $this->getPreparedAddressParams($record), + $record, + $parent_activity_id + ); //Email Entity if (!empty($record['email'])) { @@ -891,135 +899,129 @@ protected function getMembershipTypeID($record) { * @param $contact_id * @param $address_data * @param $record + * @param $parent_activity_id * * @throws \CiviCRM_API3_Exception + * @return void */ - public function createOrUpdateAddress($contact_id, $address_data, $record) { - if (empty($address_data)) return; + public function createOrUpdateAddress($contact_id, $address_data, $record, $parent_activity_id = NULL) { + // Discard empty address attributes + $address_data = array_filter($address_data); + + // If there is no address data in the record, skip address update + if (empty($address_data)) { + $this->logger->logDebug("Ignoring address update without address data for contact [$contact_id]", $record); + return; + } $config = CRM_Streetimport_Config::singleton(); - $all_fields = $config->getAllAddressAttributes(); - if (!empty($address_data['country_id'])) { - // check if fields other than country_id are set - $fields_set = FALSE; - $fields_without_country = array_diff($all_fields, ['country_id']); - foreach ($fields_without_country as $field) { - if (!empty($address_data[$field])) { - $fields_set = TRUE; - } - } - // if only country is set, skip address update - if (!$fields_set) { - $this->logger->logDebug("Ignoring address update with only country_id for contact [{$contact_id}]", $record); - return; - } + $address_attributes = $config->getAllAddressAttributes(); + $address_problems = []; + + // If only country is set, skip address update + $address_without_country = CRM_Streetimport_Utils::selectKeys( + $address_data, + array_diff($address_attributes, ['country_id']) + ); + + if (empty($address_without_country)) { + $this->logger->logDebug("Ignoring address update with only country_id for contact [$contact_id]", $record); + return; } - // check if address is complete - $address_complete = TRUE; - $required_attributes = $config->getRequiredAddressAttributes(); - foreach ($required_attributes as $required_attribute) { - if (empty($address_data[$required_attribute])) { - $address_complete = FALSE; - } + // Load the current primary address + $current_address = $this->getCurrentPrimaryAddress($contact_id, $address_attributes, $record); + + // If current and new address are identical, skip address change + if ($this->isAddressIdentical($address_data, $current_address ?? [])) { + $encoded_address = json_encode($address_data); + $this->logger->logDebug("Contact Address not Updated, old and new address is identical [$contact_id]: $encoded_address", $record); + return; + } + + // If the address is missing required attributes, report a problem + foreach ($config->getRequiredAddressAttributes() as $attr) { + if (isset($address_data[$attr])) continue; + + $this->logger->logDebug("Manual address update required for [$contact_id] due to incomplete address.", $record); + $address_problems[] = "The new address is missing the required attribute: '$attr'"; + } + + // If the primary address has changed since the TM selection, report a problem + $selection_date = civicrm_api3('Activity', 'getvalue', [ + 'id' => $parent_activity_id, + 'return' => "created_date", + ]); + + if ($this->addressChangeRecordedSince($contact_id, $selection_date, $record)) { + $this->logger->logDebug("Manual address update required for [$contact_id] due to recent address change.", $record); + $address_problems[] = "The primary address of contact [$contact_id] has changed recently"; + } + + // If the address is an invalid Austrian address, report a problem + $is_austria = trim($record['Land'] ?? '') == CRM_Streetimport_GP_Utils_Address::AUSTRIA_ISO_CODE; + + if ($is_austria && !CRM_Streetimport_GP_Utils_Address::isRealAustrianAddress($record)) { + $this->logger->logDebug("Manual address update required for [$contact_id] due to invalid address.", $record); + $address_problems[] = 'The new address was not found in the address reference'; } - if (!$address_complete) { - $this->logger->logDebug("Manual address update required for [{$contact_id}] due to incomplete address.", $record); + if (!empty($address_problems)) { return $this->createManualUpdateActivity( $contact_id, 'Manual Address Update', $record, 'activities/ManualAddressUpdate.tpl', [ - 'title' => 'Please update contact\'s address', - 'subtitle' => 'New address may be incomplete!', - 'fields' => $config->getAllAddressAttributes(), - 'address' => $address_data + 'title' => 'Please update contact\'s address', + 'subtitle' => 'The address could not be automatically created/updated', + 'problems' => $address_problems, + 'fields' => $address_attributes, + 'address' => $address_data, + 'old_address' => $current_address, ] ); } - // find the old address - $old_address_id = $this->getAddressId($contact_id, $record); - if (!$old_address_id) { - // CREATION (there is no address) + if (is_null($current_address)) { + // If there is no current primary address, create a new one $address_data['location_type_id'] = $config->getLocationTypeId(); $address_data['contact_id'] = $contact_id; $this->resolveFields($address_data, $record); $this->setProvince($address_data); - $this->logger->logDebug("Creating address for contact [{$contact_id}]: " . json_encode($address_data), $record); + + $encoded_address = json_encode($address_data); + $this->logger->logDebug("Creating address for contact [$contact_id]: $encoded_address", $record); + civicrm_api3('Address', 'create', $address_data); - $template_data = [ - 'fields' => $config->getAllAddressAttributes(), - 'address' => $address_data, - ]; - return $this->createContactUpdatedActivity( - $contact_id, - $config->translate('Contact Address Created'), - $this->renderTemplate('activities/ManualAddressUpdate.tpl', $template_data), - $record - ); - } - // load old address - $old_address = civicrm_api3('Address', 'getsingle', array('id' => $old_address_id)); + $update_title = $config->translate('Contact Address Created'); - // if old and new address is identical do nothing - if ($this->isAddressIdentical($address_data, $old_address)) { - $this->logger->logDebug("Contact Address not Updated, old and new address is identical [{$contact_id}]: " . json_encode($address_data), $record); - return; - } + $update_details = $this->renderTemplate('activities/ManualAddressUpdate.tpl', [ + 'title' => $update_title, + 'fields' => $address_attributes, + 'address' => $address_data, + 'old_address' => [], + ]); + } else { + // Otherwise update the existing address + $this->setProvince($address_data); - // check if we'd overwrite EVERY one the relevant fields - // to avoid inconsistent addresses - $full_overwrite = TRUE; - foreach ($all_fields as $field) { - if (empty($address_data[$field]) && !empty($old_address[$field])) { - $full_overwrite = FALSE; - break; - } - } + $encoded_address = json_encode($address_data); + $this->logger->logDebug("Updating address for contact [$contact_id]: $encoded_address", $record); - $isCurrentCountryAustria = !empty($record['Land']) && trim($record['Land']) == CRM_Streetimport_GP_Utils_Address::AUSTRIA_ISO_CODE; - $isRealAustriaAddress = FALSE; - if ($isCurrentCountryAustria && !empty($record['strasse']) && !empty($record['PLZ']) && !empty($record['Ort'])) { - $isRealAustriaAddress = CRM_Streetimport_GP_Utils_Address::isRealAddress( - trim($record['Ort']), - trim($record['PLZ']), - trim($record['strasse']) - ); - } + civicrm_api3('Address', 'create', [ 'id' => $current_address_id ] + $address_data); - if ($full_overwrite && (!$isCurrentCountryAustria || ($isCurrentCountryAustria && $isRealAustriaAddress))) { - // this is a proper address update - $address_data['id'] = $old_address_id; - $this->setProvince($address_data); - $this->logger->logDebug("Updating address for contact [{$contact_id}]: " . json_encode($address_data), $record); - civicrm_api3('Address', 'create', $address_data); $this->addressValidated($contact_id, $record); - $template_data = [ - 'fields' => $config->getAllAddressAttributes(), - 'address' => $address_data, - 'old_address' => $old_address - ]; - return $this->createContactUpdatedActivity( - $contact_id, - $config->translate('Contact Address Updated'), - $this->renderTemplate('activities/ManualAddressUpdate.tpl', $template_data), - $record - ); - } else { - // this would create inconsistent/invalid addresses -> manual interaction required - $this->logger->logDebug("Manual address update required for [{$contact_id}] due to invalid address.", $record); - return $this->createManualUpdateActivity( - $contact_id, 'Manual Address Update', $record, 'activities/ManualAddressUpdate.tpl', - [ - 'title' => 'Please update contact\'s address', - 'subtitle' => 'New address was not found in address reference!', - 'fields' => $config->getAllAddressAttributes(), - 'address' => $address_data, - 'old_address' => $old_address + $update_title = $config->translate('Contact Address Updated'); + + $update_details = $this->renderTemplate('activities/ManualAddressUpdate.tpl', [ + 'title' => $update_title, + 'fields' => $address_attributes, + 'address' => $address_data, + 'old_address' => $current_address, ]); } + + $this->createContactUpdatedActivity($contact_id, $update_title, $update_details, $record); } /** @@ -1300,4 +1302,22 @@ public function createResponse($contact_id, $title, $record) { $this->createResponseActivity($contact_id, $title, $record); } + /** + * Get the current primary address for a contact + * + * @param $contact_id + * @param $attributes + * @param $record + * + * @return array | null + */ + private function getCurrentPrimaryAddress($contact_id, $attributes, $record) { + $address_id = $this->getAddressId($contact_id, $record); + + if (is_null($address_id)) return NULL; + + $address = civicrm_api3('Address', 'getsingle', [ 'id' => $address_id ]); + + return CRM_Streetimport_Utils::selectKeys(array_filter($address), $attributes); + } } diff --git a/CRM/Streetimport/GP/Utils/Address.php b/CRM/Streetimport/GP/Utils/Address.php index 5ea6d89..66d6c1b 100644 --- a/CRM/Streetimport/GP/Utils/Address.php +++ b/CRM/Streetimport/GP/Utils/Address.php @@ -47,4 +47,19 @@ public static function isRealAddress($city, $postalCode, $street) { } } + /** + * Check if a record contains a real Austrian address + * + * @param $record + * + * @return bool + */ + public static function isRealAustrianAddress($record) { + return self::isRealAddress( + trim($record['Ort'] ?? ''), + trim($record['PLZ'] ?? ''), + trim($record['strasse'] ?? '') + ); + } + } diff --git a/CRM/Streetimport/ImportResult.php b/CRM/Streetimport/ImportResult.php index 9004ca9..16843a0 100755 --- a/CRM/Streetimport/ImportResult.php +++ b/CRM/Streetimport/ImportResult.php @@ -207,7 +207,7 @@ public function toAPIResult() { * @param $errorType * @see https://github.com/CiviCooP/be.aivl.streetimport/issues/11 */ - protected function createErrorActivity($message, $record, $title = "Import Error", $errorType) { + protected function createErrorActivity($message, $record, $title = "Import Error", $errorType = NULL) { try { // AVOID raising another exception leading to this very handler // create the activity diff --git a/CRM/Streetimport/Utils.php b/CRM/Streetimport/Utils.php index 1177f6b..6baff3d 100755 --- a/CRM/Streetimport/Utils.php +++ b/CRM/Streetimport/Utils.php @@ -829,4 +829,21 @@ public function createActivity($data, $record, $assigned_contact_ids=NULL) { return $activity; } + /** + * Select only specified entries from an associative array + * + * @param array $array + * @param array $key + * @return array + * @access public + * @static + */ + public static function selectKeys($array, $keys) { + return array_filter( + $array, + fn ($key) => in_array($key, $keys), + ARRAY_FILTER_USE_KEY + ); + } + } diff --git a/templates/GP/activities/ManualAddressUpdate.tpl b/templates/GP/activities/ManualAddressUpdate.tpl index 0770fa0..689f30e 100755 --- a/templates/GP/activities/ManualAddressUpdate.tpl +++ b/templates/GP/activities/ManualAddressUpdate.tpl @@ -1,6 +1,13 @@

{$title}

{if !empty($subtitle)}

{$subtitle}

+ {if !empty($problems)} + + {/if} {/if}