* @return array
*/
protected function getRequiredFields(): array {
- return [['activity_type_id' => ts('Activity Type'), 'activity_date_time' => ts('Activity Date')]];
+ return [['activity_type_id', 'activity_date_time']];
}
/**
protected static function checkRequiredFields($self, string $contactORContributionId, array $importKeys, array $errors, int $weightSum, $threshold, string $fieldMessage): array {
// FIXME: should use the schema titles, not redeclare them
$requiredFields = [
- $contactORContributionId == 'contribution_id' ? 'contribution_id' : 'contribution_contact_id' => $contactORContributionId == 'contribution_id' ? ts('Contribution ID') : ts('Contact ID'),
- 'total_amount' => ts('Total Amount'),
- 'financial_type_id' => ts('Financial Type'),
+ 'contribution_contact_id' => ts('Contact ID'),
];
foreach ($requiredFields as $field => $title) {
if (!in_array($field, $importKeys)) {
- if (empty($errors['_qf_default'])) {
- $errors['_qf_default'] = '';
- }
- if ($field == $contactORContributionId) {
- if (!($weightSum >= $threshold || in_array('external_identifier', $importKeys)) &&
- !$self->isUpdateExisting()
+ if ($field == 'contribution_contact_id') {
+ if (!($weightSum >= $threshold || in_array('external_identifier', $importKeys))
) {
$errors['_qf_default'] .= ts('Missing required contact matching fields.') . " $fieldMessage " . ts('(Sum of all weights should be greater than or equal to threshold: %1).', [1 => $threshold]) . '<br />';
}
- elseif ($self->isUpdateExisting() &&
- !(in_array('invoice_id', $importKeys) || in_array('trxn_id', $importKeys) ||
- in_array('contribution_id', $importKeys)
- )
- ) {
- $errors['_qf_default'] .= ts('Invoice ID or Transaction ID or Contribution ID are required to match to the existing contribution records in Update mode.') . '<br />';
- }
- }
- else {
- $errors['_qf_default'] .= ts('Missing required field: %1', [1 => $title]) . '<br />';
}
}
}
foreach ($ruleFields as $field => $weight) {
$fieldMessage .= ' ' . $field . '(weight ' . $weight . ')';
}
- $errors = self::checkRequiredFields($self, $contactORContributionId, $importKeys, $errors, $weightSum, $threshold, $fieldMessage);
-
- //at least one field should be mapped during update.
- if ($self->isUpdateExisting()) {
- $atleastOne = FALSE;
- foreach ($self->_mapperFields as $key => $field) {
- if (in_array($key, $importKeys) &&
- !in_array($key, [
- 'doNotImport',
- 'contribution_id',
- 'invoice_id',
- 'trxn_id',
- ])
- ) {
- $atleastOne = TRUE;
- break;
- }
- }
- if (!$atleastOne) {
- $errors['_qf_default'] .= ts('At least one contribution field needs to be mapped for update during update mode.') . '<br />';
- }
+ try {
+ $parser = $self->getParser();
+ $parser->validateMapping($fields['mapper']);
}
- }
-
- if (!empty($errors)) {
- if (!empty($errors['_qf_default'])) {
- CRM_Core_Session::setStatus($errors['_qf_default'], ts("Error"), "error");
- return $errors;
+ catch (CRM_Core_Exception $e) {
+ $errors['_qf_default'] = $e->getMessage();
+ }
+ if (!$self->isUpdateExisting()) {
+ $errors = self::checkRequiredFields($self, $contactORContributionId, $importKeys, $errors, $weightSum, $threshold, $fieldMessage);
}
}
-
- return TRUE;
+ return !empty($errors) ? $errors : TRUE;
}
/**
use Civi\Api4\Contact;
use Civi\Api4\Contribution;
+use Civi\Api4\ContributionSoft;
use Civi\Api4\Email;
use Civi\Api4\Note;
*/
protected $_newContributions;
+ protected $baseEntity = 'Contribution';
+
/**
* Get information about the provided job.
* - name
* Also 'im_provider_id' is mapped to the 'real' field name 'provider_id'
*
* @return array
- * @throws \API_Exception
*/
protected function getFieldMappings(): array {
$mappedFields = [];
* @return array
*/
public function getRequiredFields(): array {
- return ['id' => ts('Contribution ID'), ['financial_type_id' => ts('Financial Type'), 'total_amount' => ts('Total Amount')]];
+ return [[$this->getRequiredFieldsForMatch(), $this->getRequiredFieldsForCreate()]];
+ }
+
+ /**
+ * Get required fields to create a contribution.
+ *
+ * @return array
+ */
+ public function getRequiredFieldsForCreate(): array {
+ return ['financial_type_id', 'total_amount'];
+ }
+
+ /**
+ * Get required fields to match a contribution.
+ *
+ * @return array
+ */
+ public function getRequiredFieldsForMatch(): array {
+ return [['id'], ['invoice_id'], ['trxn_id']];
}
/**
}
}
+ /**
+ * Get a list of entities this import supports.
+ *
+ * @return array
+ * @throws \API_Exception
+ */
+ public function getImportEntities() : array {
+ $softCreditTypes = ContributionSoft::getFields()
+ ->setLoadOptions(TRUE)
+ ->addWhere('name', '=', 'soft_credit_type_id')
+ ->selectRowCount()
+ ->addSelect('options')->execute();
+ return [
+ 'Contribution' => [
+ 'text' => ts('Contribution Fields'),
+ 'required_fields_update' => $this->getRequiredFieldsForMatch(),
+ 'required_fields_create' => $this->getRequiredFieldsForCreate(),
+ 'is_base_entity' => TRUE,
+ // For now we stick with the action selected on the DataSource page.
+ 'actions' => $this->isUpdateExisting() ?
+ [['id' => 'update', 'text' => ts('Update existing'), 'description' => ts('Skip if no match found')]] :
+ [['id' => 'create', 'text' => ts('Create'), 'description' => ts('Skip if already exists')]],
+ 'default_action' => $this->isUpdateExisting() ? 'update' : 'create',
+ 'entity_name' => 'Contribution',
+ 'entity_title' => ts('Contribution'),
+ ],
+ 'Contact' => [
+ 'text' => ts('Contact Fields'),
+ 'unique_fields' => ['external_identifier', 'id'],
+ 'is_contact' => TRUE,
+ 'actions' => [
+ ['id' => 'select', 'text' => ts('Match existing')],
+ ['id' => 'update', 'text' => ts('Update existing'), ts('Skip if not found')],
+ ['id' => 'update_or_create', 'text' => ts('Update or Create')],
+ ],
+ 'default_action' => 'select',
+ 'entity_name' => 'Contact',
+ 'entity_title' => ts('Contribution Contact'),
+ ],
+ 'SoftCreditContact' => [
+ 'text' => ts('Soft Credit Contact Fields'),
+ 'maximum' => count($softCreditTypes),
+ 'unique_fields' => ['external_identifier', 'id'],
+ 'is_contact' => TRUE,
+ 'actions' => [
+ ['id' => 'select', 'text' => ts('Match existing')],
+ ['id' => 'update', 'text' => ts('Update existing'), 'description' => ts('Skip if not found')],
+ ['id' => 'update_or_create', 'text' => ts('Update or Create')],
+ ],
+ 'default_action' => 'select',
+ 'entity_name' => 'SoftCreditContact',
+ 'entity_title' => ts('Soft Credit Contact'),
+ 'entity_data' => ['soft_credit_type_id' => ['required' => TRUE, 'options' => $softCreditTypes]],
+ ],
+ ];
+ }
+
/**
* Combine all the importable fields from the lower levels object.
*
* @return array
*/
public function getRequiredFields(): array {
- return ['contact_id' => ts('Contact ID'), 'external_identifier' => ts('External Identifier')];
+ return [['contact_id'], ['external_identifier']];
}
/**
* @return array
*/
protected function getRequiredFields(): array {
- return [['event_id' => ts('Event'), 'status_id' => ts('Status')]];
+ return [['event_id', 'status_id']];
}
}
return [];
}
+ /**
+ * An array of Custom field mappings for api formatting
+ *
+ * e.g ['custom_7' => 'IndividualData.Marriage_date']
+ *
+ * @var array
+ */
+ protected $customFieldNameMap = [];
+
/**
* Get User Job.
*
if (!empty($params['id'])) {
return;
}
- $requiredFields = [
- 'Individual' => [
- 'first_name_last_name' => ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')],
- 'email' => ts('Email Address'),
- ],
- 'Organization' => ['organization_name' => ts('Organization Name')],
- 'Household' => ['household_name' => ts('Household Name')],
- ][$contactType];
+ $requiredFields = $this->getRequiredFieldsContactCreate()[$contactType];
if ($isPermitExistingMatchFields) {
- $requiredFields['external_identifier'] = ts('External Identifier');
// Historically just an email has been accepted as it is 'usually good enough'
// for a dedupe rule look up - but really this is a stand in for
// whatever is needed to find an existing matching contact using the
// specified dedupe rule (or the default Unsupervised if not specified).
- $requiredFields['email'] = ts('Email Address');
+ $requiredFields = $contactType === 'Individual' ? [[$requiredFields, 'external_identifier']] : [[$requiredFields, 'email', 'external_identifier']];
}
$this->validateRequiredFields($requiredFields, $params, $prefixString);
}
+ /**
+ * Get the fields required for contact create.
+ *
+ * @return array
+ */
+ protected function getRequiredFieldsContactMatch(): array {
+ return [['id', 'external_identifier']];
+ }
+
+ /**
+ * Get the fields required for contact create.
+ *
+ * @return array
+ */
+ protected function getRequiredFieldsContactCreate(): array {
+ return [
+ 'Individual' => [
+ [
+ ['first_name', 'last_name'],
+ 'email',
+ ],
+ ],
+ 'Organization' => ['organization_name'],
+ 'Household' => ['household_name'],
+ ];
+ }
+
protected function doPostImportActions() {
$userJob = $this->getUserJob();
$summaryInfo = $userJob['metadata']['summary_info'] ?? [];
* - note this follows the and / or array nesting we see in permission checks
* eg.
* [
- * 'email' => ts('Email'),
- * ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')]
+ * 'email',
+ * ['first_name', 'last_name']
* ]
* Means 'email' OR 'first_name AND 'last_name'.
* @param string $prefixString
* @throws \CRM_Core_Exception Exception thrown if field requirements are not met.
*/
protected function validateRequiredFields(array $requiredFields, array $params, $prefixString = ''): void {
- if (empty($requiredFields)) {
+ $missingFields = $this->getMissingFields($requiredFields, $params);
+ if (empty($missingFields)) {
return;
}
- $missingFields = [];
- foreach ($requiredFields as $key => $required) {
- if (!is_array($required)) {
- $importParameter = $params[$key] ?? [];
- if (!is_array($importParameter)) {
- if (!empty($importParameter)) {
- return;
- }
- }
- else {
- foreach ($importParameter as $locationValues) {
- if (!empty($locationValues[$key])) {
- return;
- }
- }
- }
+ throw new CRM_Core_Exception($prefixString . ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
+ }
- $missingFields[$key] = $required;
+ /**
+ * Validate that the mapping has the required fields.
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function validateMapping($mapping): void {
+ $mappedFields = [];
+ foreach ($mapping as $mappingField) {
+ $mappedFields[$mappingField[0]] = $mappingField[0];
+ }
+ $entity = $this->baseEntity;
+ $missingFields = $this->getMissingFields($this->getRequiredFieldsForEntity($entity, $this->getActionForEntity($entity)), $mappedFields);
+ if (!empty($missingFields)) {
+ $error = [];
+ foreach ($missingFields as $missingField) {
+ $error[] = ts('Missing required field: %1', [1 => $missingField]);
}
- else {
- foreach ($required as $field => $label) {
- if (empty($params[$field])) {
- $missing[$field] = $label;
- }
+ throw new CRM_Core_Exception(implode('<br/>', $error));
+ }
+ }
+
+ /**
+ * Get the import action for the given entity.
+ *
+ * @param string $entity
+ *
+ * @return string
+ * @throws \API_Exception
+ */
+ private function getActionForEntity(string $entity): string {
+ return $this->getUserJob()['metadata']['entity_metadata'][$entity]['action'] ?? $this->getImportEntities()[$entity]['default_action'];
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return array
+ */
+ private function getRequiredFieldsForEntity(string $entity, string $action): array {
+ $entityMetadata = $this->getImportEntities()[$entity];
+ if ($action === 'select') {
+ // Select uses the same lookup as update.
+ $action = 'update';
+ }
+ if (isset($entityMetadata['required_fields_' . $action])) {
+ return $entityMetadata['required_fields_' . $action];
+ }
+ return [];
+ }
+
+ /**
+ * Get the field requirements that are missing from the params array.
+ *
+ * Eg Must have 'total_amount' and 'financial_type_id'
+ * [
+ * 'total_amount',
+ * 'financial_type_id'
+ * ]
+ *
+ * Eg Must have 'invoice_id' or 'trxn_id' or 'id'
+ *
+ * [
+ * ['invoice_id'],
+ * ['trxn_id'],
+ * ['id']
+ * ],
+ *
+ * Eg Must have 'invoice_id' or 'trxn_id' or 'id' OR (total_amount AND financial_type_id)
+ * [
+ * [['invoice_id'], ['trxn_id'], ['id']]],
+ * ['total_amount', 'financial_type_id]
+ * ],
+ *
+ * Eg Must have 'invoice_id' or 'trxn_id' or 'id' AND (total_amount AND financial_type_id)
+ * [
+ * [['invoice_id'], ['trxn_id'], ['id']],
+ * ['total_amount', 'financial_type_id]
+ * ]
+ *
+ * @param array $requiredFields
+ * @param array $params
+ *
+ * @return array
+ */
+ protected function getMissingFields(array $requiredFields, array $params): array {
+ if (empty($requiredFields)) {
+ return [];
+ }
+ return $this->checkRequirement($requiredFields, $params);
+ }
+
+ /**
+ * Check an individual required fields criteria.
+ *
+ * @see getMissingFields
+ *
+ * @param string|array $requirement
+ * @param array $params
+ *
+ * @return array
+ */
+ private function checkRequirement($requirement, array $params): array {
+ $missing = [];
+ if (!is_array($requirement)) {
+ // In this case we need to match the field....
+ // if we do, then return empty, otherwise return
+ if (!empty($params[$requirement])) {
+ if (!is_array($params[$requirement])) {
+ return [];
}
- if (empty($missing)) {
- return;
+ // Recurse the array looking for the key - eg. look for email
+ // in a location values array
+ foreach ($params[$requirement] as $locationValues) {
+ if (!empty($locationValues[$requirement])) {
+ return [];
+ }
}
- $missingFields[$key] = implode(' ' . ts('and') . ' ', $missing);
}
+ return [$requirement => $this->getFieldMetadata($requirement)['title']];
}
- throw new CRM_Core_Exception($prefixString . ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
+
+ foreach ($requirement as $required) {
+ $isOrOperator = isset($requirement[0]) && is_array($requirement[0]);
+ $check = $this->checkRequirement($required, $params);
+ // A nested array is an 'OR' If we find any one then return.
+ if ($isOrOperator && empty($check)) {
+ return [];
+ }
+ $missing = array_merge($missing, $check);
+ }
+ if (!empty($missing)) {
+ $separator = ' ' . ($isOrOperator ? ts('OR') : ts('and')) . ' ';
+ return [implode($separator, $missing)];
+ }
+ return [];
}
/**
*
* @param string $key
*
+ * @return string
+ *
* @throws \CRM_Core_Exception
*/
protected function getApi4Name(string $key): string {
- return Contact::getFields(FALSE)
- ->addWhere('custom_field_id', '=', $this->getFieldMetadata($key)['custom_field_id'])
- ->addSelect('name')
- ->execute()->first()['name'];
+ if (!isset($this->customFieldNameMap[$key])) {
+ $this->customFieldNameMap[$key] = Contact::getFields(FALSE)
+ ->addWhere('custom_field_id', '=', str_replace('custom_', '', $key))
+ ->addSelect('name')
+ ->execute()->first()['name'];
+ }
+ return $this->customFieldNameMap[$key];
}
/**
'individual_required' => [
'csv' => 'individual_invalid_missing_name.csv',
'mapper' => [['last_name']],
- 'expected_error' => 'Missing required fields: First Name OR Email Address',
+ 'expected_error' => 'Missing required fields: First Name OR Email',
],
'individual_related_required_met' => [
'csv' => 'individual_valid_with_related_email.csv',
'individual_related_required_not_met' => [
'csv' => 'individual_invalid_with_related_phone.csv',
'mapper' => [['first_name'], ['last_name'], ['1_a_b', 'phone', 1, 2]],
- 'expected_error' => '(Child of) Missing required fields: First Name and Last Name OR Email Address OR External Identifier',
+ 'expected_error' => '(Child of) Missing required fields: First Name and Last Name OR Email OR External Identifier',
],
'individual_bad_email' => [
'csv' => 'individual_invalid_email.csv',
// External identifier is only enough in upgrade mode.
'csv' => 'individual_invalid_external_identifier_only.csv',
'mapper' => [['external_identifier'], ['gender_id']],
- 'expected_error' => 'Missing required fields: First Name and Last Name OR Email Address',
+ 'expected_error' => 'Missing required fields: First Name and Last Name OR Email',
],
'individual_invalid_external_identifier_only_update_mode' => [
// External identifier only enough in upgrade mode, so no error here.
$contactID = $this->individualCreate();
$values = ['contribution_contact_id' => $contactID, 'total_amount' => 10, 'financial_type_id' => 'Donation', 'payment_instrument_id' => 'Check', 'contribution_status_id' => 'Pending'];
// Note that the expected result should logically be CRM_Import_Parser::valid but writing test to reflect not fix here
- $this->runImport($values, CRM_Import_Parser::DUPLICATE_SKIP, NULL);
+ $this->runImport($values, CRM_Import_Parser::DUPLICATE_SKIP);
$contribution = $this->callAPISuccessGetSingle('Contribution', ['contact_id' => $contactID]);
$this->assertEquals('Pending Label**', $contribution['contribution_status']);
$contactID = $this->individualCreate();
$values = ['contribution_contact_id' => $contactID, 'total_amount' => 10, 'financial_type_id' => 'Donation', 'payment_instrument_id' => 'Check', 'contribution_status_id' => 'Pending'];
// Note that the expected result should logically be CRM_Import_Parser::valid but writing test to reflect not fix here
- $this->runImport($values, CRM_Import_Parser::DUPLICATE_SKIP, NULL);
+ $this->runImport($values, CRM_Import_Parser::DUPLICATE_SKIP);
$contribution = $this->callAPISuccess('Contribution', 'getsingle', ['contact_id' => $contactID]);
$this->createCustomGroupWithFieldOfType([], 'radio');
$values['contribution_id'] = $contribution['id'];
$values[$this->getCustomFieldName('radio')] = 'Red Testing';
unset(Civi::$statics['CRM_Core_BAO_OptionGroup']);
- $this->runImport($values, CRM_Import_Parser::DUPLICATE_UPDATE, NULL);
+ $this->runImport($values, CRM_Import_Parser::DUPLICATE_UPDATE);
$contribution = $this->callAPISuccess('Contribution', 'get', ['contact_id' => $contactID, $this->getCustomFieldName('radio') => 'Red Testing']);
$this->assertEquals(5, $contribution['values'][$contribution['id']]['custom_' . $this->ids['CustomField']['radio']]);
$this->callAPISuccess('CustomField', 'delete', ['id' => $this->ids['CustomField']['radio']]);
$values = ['contribution_contact_id' => $contactID, 'total_amount' => 10, 'financial_type_id' => 'Donation', $customField => 'L,V'];
$this->runImport($values, CRM_Import_Parser::DUPLICATE_SKIP, NULL);
$initialContribution = $this->callAPISuccessGetSingle('Contribution', ['contact_id' => $contactID]);
- $this->assertContains('L', $initialContribution[$customField], "Contribution Duplicate Skip Import contains L");
- $this->assertContains('V', $initialContribution[$customField], "Contribution Duplicate Skip Import contains V");
+ $this->assertContains('L', $initialContribution[$customField], 'Contribution Duplicate Skip Import contains L');
+ $this->assertContains('V', $initialContribution[$customField], 'Contribution Duplicate Skip Import contains V');
// Now update.
$values['contribution_id'] = $initialContribution['id'];
$this->assertEquals($anthony, $contribution['contact_id']);
}
+ /**
+ * Test that a trxn_id is enough in update mode to void the total_amount requirement.
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function testImportFieldsNotRequiredWithTrxnID(): void {
+ $this->individualCreate(['email' => 'mum@example.com']);
+ $fieldMappings = [
+ ['name' => 'first_name'],
+ ['name' => ''],
+ ['name' => 'receive_date'],
+ ['name' => 'financial_type_id'],
+ ['name' => 'email'],
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => 'trxn_id'],
+ ];
+ // First we try to create without total_amount mapped.
+ // It will fail in create mode as total_amount is required for create.
+ $this->submitDataSourceForm('contributions.csv', $fieldMappings, ['onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP]);
+ $form = $this->getMapFieldForm([
+ 'onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP,
+ 'mapper' => $this->getMapperFromFieldMappings($fieldMappings),
+ 'contactType' => CRM_Import_Parser::CONTACT_INDIVIDUAL,
+ ]);
+ $form->setUserJobID($this->userJobID);
+ $form->buildForm();
+ $this->assertFalse($form->validate());
+ $this->assertEquals(['_qf_default' => 'Missing required field: Total Amount'], $form->_errors);
+
+ // Now we add in total amount - it works in create mode.
+ $fieldMappings[1]['name'] = 'total_amount';
+ $this->importCSV('contributions.csv', $fieldMappings, ['onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP]);
+
+ $row = $this->getDataSource()->getRows()[0];
+ $this->assertEquals('IMPORTED', $row[9]);
+ $contribution = Contribution::get()->addSelect('source', 'id')->execute()->first();
+ $this->assertEmpty($contribution['source']);
+
+ // Now we re-import as an update, only setting the 'source' field.
+ $fieldMappings = [
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => ''],
+ ['name' => 'contribution_source'],
+ ['name' => 'trxn_id'],
+ ];
+ $this->importCSV('contributions.csv', $fieldMappings, ['onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE]);
+
+ $row = $this->getDataSource()->getRows()[0];
+ $this->assertEquals('IMPORTED', $row[9]);
+ $contribution = Contribution::get()->addSelect('source', 'id')->execute()->first();
+ $this->assertEquals('Call him back', $contribution['source']);
+ }
+
/**
* @throws \CRM_Core_Exception
*/
/**
* @param array $submittedValues
*
- * @return array
+ * @return int
*
* @throws \API_Exception
* @throws \CRM_Core_Exception
* @param array $submittedValues
*/
protected function importCSV(string $csv, array $fieldMappings, array $submittedValues = []): void {
- $reflector = new ReflectionClass(get_class($this));
- $directory = dirname($reflector->getFileName());
$submittedValues = array_merge([
- 'uploadFile' => ['name' => $directory . '/data/' . $csv],
'skipColumnHeader' => TRUE,
'fieldSeparator' => ',',
'contactType' => CRM_Import_Parser::CONTACT_INDIVIDUAL,
'onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP,
'groups' => [],
], $submittedValues);
- $form = $this->getDataSourceForm($submittedValues);
- $values = $_SESSION['_' . $form->controller->_name . '_container']['values'];
- $form->buildForm();
- $form->postProcess();
- $this->userJobID = $form->getUserJobID();
- // This gets reset in DataSource so re-do....
- $_SESSION['_' . $form->controller->_name . '_container']['values'] = $values;
+ $this->submitDataSourceForm($csv, $submittedValues);
$form = $this->getMapFieldForm($submittedValues);
$form->setUserJobID($this->userJobID);
return new CRM_Import_DataSource_CSV($this->userJobID);
}
+ /**
+ * Submit the data source form.
+ *
+ * @param string $csv
+ * @param array $submittedValues
+ */
+ protected function submitDataSourceForm(string $csv, $submittedValues): void {
+ $reflector = new ReflectionClass(get_class($this));
+ $directory = dirname($reflector->getFileName());
+ $submittedValues = array_merge([
+ 'uploadFile' => ['name' => $directory . '/data/' . $csv],
+ 'skipColumnHeader' => TRUE,
+ 'fieldSeparator' => ',',
+ 'contactType' => CRM_Import_Parser::CONTACT_INDIVIDUAL,
+ 'dataSource' => 'CRM_Import_DataSource_CSV',
+ 'file' => ['name' => $csv],
+ 'dateFormats' => CRM_Core_Form_Date::DATE_yyyy_mm_dd,
+ 'onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP,
+ 'groups' => [],
+ ], $submittedValues);
+ $form = $this->getDataSourceForm($submittedValues);
+ $values = $_SESSION['_' . $form->controller->_name . '_container']['values'];
+ $form->buildForm();
+ $form->postProcess();
+ $this->userJobID = $form->getUserJobID();
+ // This gets reset in DataSource so re-do....
+ $_SESSION['_' . $form->controller->_name . '_container']['values'] = $values;
+ }
+
}