From 6b0a7c820050e99b7fe508be1ac1941afec3f816 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Tue, 30 Aug 2022 09:40:38 +1200 Subject: [PATCH] Add angular form to civiimport extension --- CRM/Contribute/Import/Parser/Contribution.php | 12 +- CRM/Import/Forms.php | 31 +- CRM/Import/Parser.php | 2 +- ext/civiimport/ang/crmCiviimport.ang.php | 24 ++ ext/civiimport/ang/crmCiviimport.css | 1 + ext/civiimport/ang/crmCiviimport.js | 310 ++++++++++++++++++ ext/civiimport/ang/crmCiviimport/Import.html | 149 +++++++++ ext/civiimport/civiimport.php | 53 +++ ext/civiimport/info.xml | 1 + .../templates/CRM/Import/MapField.tpl | 18 + .../CRM/crmCiviimport/ImportUiCtrl.hlp | 1 + .../Import/Parser/ContributionTest.php | 1 + .../Parser/data/soft_credit_extended.csv | 6 +- 13 files changed, 595 insertions(+), 14 deletions(-) create mode 100644 ext/civiimport/ang/crmCiviimport.ang.php create mode 100644 ext/civiimport/ang/crmCiviimport.css create mode 100644 ext/civiimport/ang/crmCiviimport.js create mode 100644 ext/civiimport/ang/crmCiviimport/Import.html create mode 100644 ext/civiimport/templates/CRM/Import/MapField.tpl create mode 100644 ext/civiimport/templates/CRM/crmCiviimport/ImportUiCtrl.hlp diff --git a/CRM/Contribute/Import/Parser/Contribution.php b/CRM/Contribute/Import/Parser/Contribution.php index 4473327894..03eabb63de 100644 --- a/CRM/Contribute/Import/Parser/Contribution.php +++ b/CRM/Contribute/Import/Parser/Contribution.php @@ -232,7 +232,8 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser { } else { if ($entity === 'Contact' && !isset($params[$entity])) { - $params[$entity] = $this->getContactType() ? ['contact_type' => $this->getContactType()] : []; + $params[$entity] = []; + $params[$entity]['contact_type'] = $this->getContactTypeForEntity($entity) ?: $this->getContactType(); } $params[$entity][$this->getFieldMetadata($mappedField['name'])['name']] = $this->getTransformedFieldValue($mappedField['name'], $fieldValue); } @@ -366,6 +367,7 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser { 'default_action' => $this->isUpdateExisting() ? 'update' : 'create', 'entity_name' => 'Contribution', 'entity_title' => ts('Contribution'), + 'entity_field_prefix' => '', 'selected' => ['action' => $this->isUpdateExisting() ? 'update' : 'create'], ], 'Contact' => [ @@ -377,8 +379,9 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser { 'selected' => [ 'action' => $this->isUpdateExisting() ? 'ignore' : 'select', 'contact_type' => $this->getSubmittedValue('contactType'), - 'dedupe_rule' => $this->getDedupeRule($this->getSubmittedValue('contactType'))['name'], + 'dedupe_rule' => $this->getDedupeRule($this->getContactType())['name'], ], + 'entity_field_prefix' => '', 'default_action' => 'select', 'entity_name' => 'Contact', 'entity_title' => ts('Contribution Contact'), @@ -391,9 +394,10 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser { 'is_contact' => TRUE, 'is_required' => FALSE, 'actions' => array_merge([['id' => 'ignore', 'text' => ts('Do not import')]], $this->getActions(['select', 'update', 'save'])), - 'selected' => ['contact_type' => '', 'soft_credit_type_id' => '', 'action' => 'ignore'], + 'selected' => ['contact_type' => '', 'soft_credit_type_id' => reset($softCreditTypes)['id'], 'action' => 'ignore'], 'default_action' => 'ignore', 'entity_name' => 'SoftCreditContact', + 'entity_field_prefix' => 'soft_credit.contact.', 'entity_title' => ts('Soft Credit Contact'), 'entity_data' => [ 'soft_credit_type_id' => [ @@ -431,7 +435,7 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser { if (empty($contributionParams['id']) && $this->isUpdateExisting()) { throw new CRM_Core_Exception('Empty Contribution and Invoice and Transaction ID. Row was skipped.', CRM_Import_Parser::ERROR); } - $contributionParams['contact_id'] = $this->getContactID($params['Contact'] ?? [], $contributionParams['contact_id'] ?? ($existingContribution['contact_id'] ?? NULL), 'Contact', $this->getDedupeRulesForEntity('Contact')); + $contributionParams['contact_id'] = $params['Contact']['id'] = $this->getContactID($params['Contact'] ?? [], $contributionParams['contact_id'] ?? ($existingContribution['contact_id'] ?? NULL), 'Contact', $this->getDedupeRulesForEntity('Contact')); $softCreditParams = []; foreach ($params['SoftCreditContact'] ?? [] as $index => $softCreditContact) { diff --git a/CRM/Import/Forms.php b/CRM/Import/Forms.php index e0ef962e28..5f5cec0fde 100644 --- a/CRM/Import/Forms.php +++ b/CRM/Import/Forms.php @@ -341,12 +341,7 @@ class CRM_Import_Forms extends CRM_Core_Form { * @throws \CRM_Core_Exception */ protected function getContactType(): string { - $contactTypeMapping = [ - 'Individual' => 'Individual', - 'Household' => 'Household', - 'Organization' => 'Organization', - ]; - return $contactTypeMapping[$this->getSubmittedValue('contactType')]; + return $this->getSubmittedValue('contactType') ?? $this->getUserJob()['metadata']['entity_configuration']['Contact']['contact_type']; } /** @@ -769,4 +764,28 @@ class CRM_Import_Forms extends CRM_Core_Form { return reset($info)['entity']; } + /** + * Assign values for civiimport. + * + * I wanted to put this in the extension - but there are a lot of protected functions + * we would need to revisit and make public - do we want to? + * + * @throws \CRM_Core_Exception + */ + public function assignCiviimportVariables(): void { + $contactTypes = []; + foreach (CRM_Contact_BAO_ContactType::basicTypeInfo() as $contactType) { + $contactTypes[] = ['id' => $contactType['name'], 'text' => $contactType['label']]; + } + $parser = $this->getParser(); + Civi::resources()->addVars('crmImportUi', [ + 'defaults' => $this->getDefaults(), + 'rows' => $this->getDataRows([], 2), + 'contactTypes' => $contactTypes, + 'entityMetadata' => $this->getFieldOptions(), + 'dedupeRules' => $parser->getAllDedupeRules(), + 'userJob' => $this->getUserJob(), + ]); + } + } diff --git a/CRM/Import/Parser.php b/CRM/Import/Parser.php index 18aa62af1a..65eba6fdf1 100644 --- a/CRM/Import/Parser.php +++ b/CRM/Import/Parser.php @@ -204,7 +204,7 @@ abstract class CRM_Import_Parser implements UserJobInterface { * @return string */ protected function getContactType(): string { - return $this->getSubmittedValue('contactType'); + return $this->getSubmittedValue('contactType') ?: $this->getContactTypeForEntity('Contact'); } /** diff --git a/ext/civiimport/ang/crmCiviimport.ang.php b/ext/civiimport/ang/crmCiviimport.ang.php new file mode 100644 index 0000000000..267a8394e3 --- /dev/null +++ b/ext/civiimport/ang/crmCiviimport.ang.php @@ -0,0 +1,24 @@ + [ + 'ang/crmCiviimport.js', + 'ang/crmCiviimport/*.js', + 'ang/crmCiviimport/*/*.js', + ], + 'css' => [ + 'ang/crmCiviimport.css', + ], + 'partials' => [ + 'ang/crmCiviimport', + ], + 'requires' => [ + 'crmUi', + 'crmUtil', + 'ngRoute', + 'api4', + ], + 'settings' => [], +]; diff --git a/ext/civiimport/ang/crmCiviimport.css b/ext/civiimport/ang/crmCiviimport.css new file mode 100644 index 0000000000..b4b42467a1 --- /dev/null +++ b/ext/civiimport/ang/crmCiviimport.css @@ -0,0 +1 @@ +/* Add any CSS rules for Angular module "crmCiviimport" */ diff --git a/ext/civiimport/ang/crmCiviimport.js b/ext/civiimport/ang/crmCiviimport.js new file mode 100644 index 0000000000..5927a2ab98 --- /dev/null +++ b/ext/civiimport/ang/crmCiviimport.js @@ -0,0 +1,310 @@ +(function(angular, $, _) { + // Declare a list of dependencies. + angular.module('crmCiviimport', CRM.angRequires('crmCiviimport')); + + // The controller uses *injection*. This default injects a few things: + // $scope -- This is the set of variables shared between JS and HTML. + // crmApi, crmStatus, crmUiHelp -- These are services provided by civicrm-core. + // myContact -- The current contact, defined above in config(). + angular.module('crmCiviimport').component('crmImportUi', { + templateUrl: '~/crmCiviimport/Import.html', + controller: function($scope, crmApi4, crmStatus, crmUiHelp) { + + // The ts() and hs() functions help load strings for this module. + var ts = $scope.ts = CRM.ts('civiimport'); + var hs = $scope.hs = crmUiHelp({file: 'CRM/crmCiviimport/crmImportUi'}); + // Local variable for this controller (needed when inside a callback fn where `this` is not available). + var ctrl = this; + + $scope.load = (function () { + // The components of crmImportUi that we use are assigned individually for clarity - but + // don't seem to work without the first assignment.... + $scope.data = CRM.vars.crmImportUi; + $scope.data.rows = CRM.vars.crmImportUi.rows; + $scope.data.entityMetadata = CRM.vars.crmImportUi.entityMetadata; + // The defaults here are derived in the php layer from the saved mapping or the column + // headers. The latter involves some regex. + $scope.data.defaults = CRM.vars.crmImportUi.defaults; + $scope.userJob = CRM.vars.crmImportUi.userJob; + $scope.data.showColumnNames = $scope.userJob.metadata.submitted_values.skipColumnHeader; + $scope.data.savedMapping = CRM.vars.crmImportUi.savedMapping; + $scope.mappingSaving = {updateFieldMapping: 0, newFieldMapping: 0}; + // Used for dedupe rules select options, also for filtering available fields. + $scope.data.dedupeRules = CRM.vars.crmImportUi.dedupeRules; + // Used for select contact type select-options. + $scope.data.contactTypes = CRM.vars.crmImportUi.contactTypes; + + $scope.data.entities = {}; + // Available entities is entityMetadata mapped to a form-friendly format + $scope.entitySelection = []; + var entityConfiguration = $scope.userJob.metadata.entity_configuration; + _.each($scope.data.entityMetadata, function (entityMetadata) { + var selected = Boolean(entityConfiguration) ? entityConfiguration[entityMetadata.entity_name] : entityMetadata.selected; + // If our selected action is not available then fall back to the entity default. + // This would happen if we went back to the DataSource screen & made a change, as the + // php layer filters on that configuration options + var isActionValid = entityMetadata.actions.filter((function (action) { + if (action.id === selected.action) { + return true; + } + })); + if (isActionValid.length === 0) { + // Selected action not available, go back to the default. + selected.action = entityMetadata.selected.action; + } + + entityMetadata.dedupe_rules = []; + if (Boolean(entityMetadata.selected) && Boolean(selected.contact_type)) { + entityMetadata.dedupe_rules = $scope.getDedupeRules(selected.contact_type); + } + + $scope.entitySelection.push({ + id: entityMetadata.entity_name, + text: entityMetadata.entity_title, + actions: entityMetadata.actions, + is_contact: Boolean(entityMetadata.is_contact), + entity_data: entityMetadata.entity_data, + dedupe_rules: entityMetadata.dedupe_rules, + }); + $scope.addEntity(entityMetadata.entity_name, selected); + }); + + function buildImportMappings() { + $scope.data.importMappings = []; + var importMappings = $scope.userJob.metadata.import_mappings; + _.each($scope.userJob.metadata.DataSource.column_headers, function (header, index) { + var fieldName = $scope.data.defaults['mapper[' + index + ']'][0]; + if (Boolean(fieldName)) { + fieldName = fieldName.replace('__', '.'); + } + var fieldDefault = null; + + if (Boolean(importMappings)) { + // If this form has already been used for the job, load from what it saved. + fieldName = importMappings[index].name; + fieldDefault = importMappings[index].default_value; + } + $scope.data.importMappings.push({ + header: header, + selectedField: fieldName, + defaultValue: fieldDefault + }); + }); + } + + buildImportMappings(); + + }); + + /** + * Get fields available to map to. + * + * @type {function(): {results: $scope.data.entityMetadata}} + */ + $scope.getFields = (function () { + var fields = []; + // The $scope.data.entityMetadata entity array has all available fields. + // - for field filtering we have to start with the full array or it just gets smaller & smaller. + _.each($scope.data.entityMetadata, function (entity) { + // The $scope.data.entities has the selected data (but the fields are already filtered) + var selected = $scope.data.entities[entity.entity_name].selected; + if (selected.action !== 'ignore') { + availableEntity = _.clone(entity); + availableEntity.children = $scope.filterEntityFields(entity.is_contact, entity.children, selected, entity.entity_field_prefix); + fields.push(availableEntity); + } + }); + return {results: fields}; + }); + + /** + * Filter the fields available for the entity based on form selections. + * + * Currently we only filter contact fields here, based on contact type, dedupe rule, + * and action. + * + * @type {(function(*=, *=, *=, *=): (*))|*} + */ + $scope.filterEntityFields = (function (isContact, fields, selection, entityFieldPrefix) { + if (isContact) { + return $scope.filterContactFields(fields, selection, entityFieldPrefix); + } + return fields; + }); + + /** + * Filter contact fields, removing fields not appropriate for the entity or action. + * + * @type {function(*=, *): *} + */ + $scope.filterContactFields = (function (fields, selection, entityFieldPrefix) { + var contactType = selection.contact_type; + var action = selection.action; + var rules = $scope.data.dedupeRules; + var dedupeRule = rules[selection.dedupe_rule]; + fields = fields.filter((function (field) { + // Using replace here is safe ... for now... cos only soft credits have a prefix + // but if we add a prefix to contact this will need updating. + var fieldName = field.id.replace(entityFieldPrefix, ''); + if (action === 'select' && !Boolean(field.match_rule) && + (!Boolean(dedupeRule) || !Boolean(dedupeRule.fields[fieldName])) + ) { + // In select mode only fields used to look up the contact are returned. + return false; + } + if (Boolean(contactType)) { + var supportedTypes = field.contact_type; + return supportedTypes[contactType]; + } + // No contact type specified, do not filter on it. + return true; + + })); + return fields; + }); + + /** + * Add the entity to the selected scope. + */ + $scope.addEntity = function (selectedEntity, selected) { + if ($scope.data.entities[selectedEntity] === undefined) { + var entityData = $scope.getEntityMetadata(selectedEntity); + entityData.selected = selected; + if (entityData.id !== undefined) { + $scope.data.entities[selectedEntity] = entityData; + } + } + }; + + /** + * Get metadata for the given entity. + * + * @param selectedEntity + * @returns {*[]} + */ + $scope.getEntityMetadata = function (selectedEntity) { + var entityData = {}; + _.each($scope.entitySelection, function (entityDetails) { + if (entityDetails.id === selectedEntity) { + + entityData = entityDetails; + return false; + } + }); + return entityData; + }; + + /** + * Get a list of dedupe rules for the entity type. + * + * @param selectedEntity + * @returns {{}} + * e.g {{name: 'IndividualSupervised', 'text' : 'Name and email', 'is_default' : true}} + */ + $scope.getDedupeRules = function (selectedEntity) { + var dedupeRules = []; + _.each($scope.data.dedupeRules, function (rule) { + if (rule.contact_type === selectedEntity) { + dedupeRules.push({'id': rule.name, 'text': rule.title, 'is_default': rule.used === 'Unsupervised'}); + } + }); + return dedupeRules; + }; + + /** + * Get the entity for the given field. + * + * @type {$scope.getEntityForField} + */ + $scope.getEntityForField = (function (fieldName) { + var entityName = ''; + _.each($scope.data.entityMetadata, function (fields) { + _.each(fields.children, function (field) { + if (field.id === fieldName) { + entityName = fields.entity_name; + return false; + } + }); + }); + return entityName; + }); + + $scope.toggleMappingFields = (function (fieldName, extra) { + if (fieldName === 'updateFieldMapping' && $scope.mappingSaving.updateFieldMapping === 0) { + $scope.mappingSaving.newFieldMapping = 0; + } + if (fieldName === 'newFieldMapping' && $scope.mappingSaving.newFieldMapping === 0) { + $scope.mappingSaving.updateFieldMapping = 0; + } + }); + + /** + * Save the user job configuration on save. + * + * We add two arrays to the 'metadata' key. This is in the format returned from `Parser->getFieldMappings()` + * and is combined with quick form data in that function. In addition to the values permitted by + * the quickForm 'default_value' is supported. + * - import mappings. e.g + * ['name' => 'financial_type_id', default_value' => 'Cash'], + * ['name' => 'soft_credit.contact.external_identifier', 'default_value' => '', 'entity_data' => ['soft_credit' => ['soft_credit_type_id => 7]], + * ... + * - entity_configuration + * + * @type {$scope.save} + */ + $scope.save = (function ($event) { + $scope.userJob.metadata.entity_configuration = {}; + $scope.userJob.metadata.import_mappings = []; + _.each($scope.entitySelection, function (entity) { + $scope.userJob.metadata.entity_configuration[entity.id] = entity.selected; + }); + _.each($scope.data.importMappings, function (importRow, index) { + selectedEntity = $scope.getEntityForField(importRow.selectedField); + var entityConfig = {}; + if (selectedEntity === 'SoftCreditContact') { + // For now we just hard-code this - mapping to soft_credit a bit undefined - but + // we are mimicking getMappingFieldFromMapperInput on the php layer. + // Could get it from entity_data but .... later. + entityConfig = {'soft_credit': $scope.userJob.metadata.entity_configuration[selectedEntity].entity.entity_data}; + } + + $scope.userJob.metadata.import_mappings.push({ + name: importRow.selectedField, + default_value: importRow.defaultValue, + // At this stage column_number is thrown away but we store it here to have it for when we change that. + column_number: index, + entity_data: entityConfig + }); + }); + crmApi4('UserJob', 'save', {records: [$scope.userJob]}); + }); + + $scope.load(); + } + } + ); + + /** + * This component is for the specific entity within the entity ng-repeat. + */ + angular.module('crmCiviimport').controller('crmImportUiEntity', function($scope) { + /** + * Get the available dedupe rules. + * + * @type {function(*): []|*} + */ + $scope.getDedupeRule = (function() { + return {results: $scope.entity.dedupe_rules}; + }); + + /** + * Update the metadata module after a change. + * + * @type {$scope.updateContactType} + */ + $scope.updateContactType = (function(entity) { + entity.dedupe_rules = $scope.getDedupeRules(entity.selected.contact_type); + entity.selected.dedupe_rule = entity.dedupe_rules[0].id; + }); + }); +})(angular, CRM.$, CRM._); diff --git a/ext/civiimport/ang/crmCiviimport/Import.html b/ext/civiimport/ang/crmCiviimport/Import.html new file mode 100644 index 0000000000..bd623388b7 --- /dev/null +++ b/ext/civiimport/ang/crmCiviimport/Import.html @@ -0,0 +1,149 @@ +
+
+ + + +
+

{{:: ts("Review the values shown below from the first 2 rows of your import file and select the matching CiviCRM database fields from the drop-down lists in the right-hand column. Select '- do not import -' for any columns in the import file that you want ignored.") }}

+

{{:: ts("If you think you may be importing additional data from the same data source, check 'Save this field mapping' at the bottom of the page before continuing. The saved mapping can then be easily reused the next time data is imported.") }}

+
+ +
+ + + +
+ +
+

{{:: ts('Import to') }}

+ +
+
+
+
{{ entity.text }} + +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ {{:: ts('Data mapping') }} + + + + + + + + + + + + + + + +
+ {{:: ts('Saved Field Mapping: %1', {1: data.savedMapping.name}) }}
{{:: ts('Column Names') }} + {{:: ts('Import Data (row %1)', {1: $index+1}) }} + {{:: ts('Matching CiviCRM Field') }}{{:: ts('Default value') }}
{{ row['header'] }} + {{ rowValue }} + +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ {{:: ts('Update this field mapping') }} + + + + +
+ + + + + + + + + +
{{:: ts('Name') }}
{{:: ts('Description') }}
+
+
+ +
+ + + +
+ diff --git a/ext/civiimport/civiimport.php b/ext/civiimport/civiimport.php index 6091d234b9..ff789bf10c 100644 --- a/ext/civiimport/civiimport.php +++ b/ext/civiimport/civiimport.php @@ -1,5 +1,7 @@ addModules('crmCiviimport'); + $form->assignCiviimportVariables(); + $savedMappingID = (int) $form->getSubmittedValue('savedMapping'); + $savedMapping = []; + if ($savedMappingID) { + $savedMapping = Mapping::get()->addWhere('id', '=', $savedMappingID)->addSelect('id', 'name', 'description')->execute()->first(); + } + Civi::resources()->addVars('crmImportUi', ['savedMapping' => $savedMapping]); + } + + if ($formName === 'CRM_Contribute_Import_Form_DataSource') { + // If we have already configured contact type on the import screen + // we remove it from the DataSource screen. + $userJobID = $form->get('user_job_id'); + if ($userJobID) { + $metadata = UserJob::get()->addWhere('id', '=', $userJobID)->addSelect('metadata')->execute()->first()['metadata']; + $contactType = $metadata['entity_configuration']['Contact']['contact_type'] ?? NULL; + if ($contactType) { + $form->removeElement('contactType'); + } + } + } +} diff --git a/ext/civiimport/info.xml b/ext/civiimport/info.xml index 1ebcca5d24..01409bdbc3 100644 --- a/ext/civiimport/info.xml +++ b/ext/civiimport/info.xml @@ -32,5 +32,6 @@ mgd-php@1.0.0 setting-php@1.0.0 + ang-php@1.0.0 diff --git a/ext/civiimport/templates/CRM/Import/MapField.tpl b/ext/civiimport/templates/CRM/Import/MapField.tpl new file mode 100644 index 0000000000..e2d5ad0ac7 --- /dev/null +++ b/ext/civiimport/templates/CRM/Import/MapField.tpl @@ -0,0 +1,18 @@ +{* + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC. All rights reserved. | + | | + | This work is published under the GNU AGPLv3 license with some | + | permitted exceptions and without any warranty. For full license | + | and copyright information, see https://civicrm.org/licensing | + +--------------------------------------------------------------------+ +*} +{* Import Wizard - Step 2 (map incoming data fields) *} +
+ {* WizardHeader.tpl provides visual display of steps thru the wizard as well as title for current step *} + {include file="CRM/common/WizardHeader.tpl"} + + + + +
diff --git a/ext/civiimport/templates/CRM/crmCiviimport/ImportUiCtrl.hlp b/ext/civiimport/templates/CRM/crmCiviimport/ImportUiCtrl.hlp new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/ext/civiimport/templates/CRM/crmCiviimport/ImportUiCtrl.hlp @@ -0,0 +1 @@ + diff --git a/tests/phpunit/CRM/Contribute/Import/Parser/ContributionTest.php b/tests/phpunit/CRM/Contribute/Import/Parser/ContributionTest.php index 80235feb19..d6ce4319d6 100644 --- a/tests/phpunit/CRM/Contribute/Import/Parser/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/Import/Parser/ContributionTest.php @@ -210,6 +210,7 @@ class CRM_Contribute_Import_Parser_ContributionTest extends CiviUnitTestCase { ['name' => 'financial_type_id', 'default_value' => 'Donation'], ['name' => 'contribution_source'], ['name' => 'receive_date'], + ['name' => 'external_identifier'], ['name' => 'soft_credit.contact.email_primary.email', 'entity_data' => ['soft_credit' => ['soft_credit_type_id' => 5]]], ['name' => 'soft_credit.contact.first_name', 'entity_data' => ['soft_credit' => ['soft_credit_type_id' => 5]]], ['name' => 'soft_credit.contact.last_name', 'entity_data' => ['soft_credit' => ['soft_credit_type_id' => 5]]], diff --git a/tests/phpunit/CRM/Contribute/Import/Parser/data/soft_credit_extended.csv b/tests/phpunit/CRM/Contribute/Import/Parser/data/soft_credit_extended.csv index ac9fca6815..a7bdb279ac 100644 --- a/tests/phpunit/CRM/Contribute/Import/Parser/data/soft_credit_extended.csv +++ b/tests/phpunit/CRM/Contribute/Import/Parser/data/soft_credit_extended.csv @@ -1,3 +1,3 @@ -Organization Name,Legal Name,Amount,Financial Type,Source,Date,Soft credi contact email,Soft credit first name,Soft Credit last name -Big Firm,Big Firm inc.,800,,Import,2022-09-08,jenny@example.org,Jenny,Hawthorn -Small Firm,Small Firm inc.,70,Check,Import,2022-09-08,sarah@example.org,Sarah,Windsor +Organization Name,Legal Name,Amount,Financial Type,Source,Received Date,External identifier,Soft credit email,Soft credit first name,Soft Credit last name +Big Firm,Big Firm inc.,800,,Import,2022-09-08,abc,jenny@example.org,Jenny,Hawthorn +Small Firm,Small Firm inc.,70,Donation,Import,2022-09-08,zyx,sarah@example.org,Sarah,Windsor -- 2.25.1