diff --git a/app/Support/Import/Configuration/File/ConfigureMappingHandler.php b/app/Support/Import/Configuration/File/ConfigureMappingHandler.php index 5e5025c4e2..e7c3efe693 100644 --- a/app/Support/Import/Configuration/File/ConfigureMappingHandler.php +++ b/app/Support/Import/Configuration/File/ConfigureMappingHandler.php @@ -229,7 +229,7 @@ class ConfigureMappingHandler implements ConfigurationInterface $preProcessClass = config(sprintf('csv.import_roles.%s.pre-process-mapper', $column)); if (null !== $hasPreProcess && true === $hasPreProcess && null !== $preProcessClass) { - $name = sprintf('\\FireflyIII\\Import\\MapperPreProcess\\%s', $preProcessClass); + $name = sprintf('FireflyIII\\Import\\MapperPreProcess\\%s', $preProcessClass); } return $name; diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index da40907a9c..497816a51c 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -36,71 +36,71 @@ use Log; class ImportTransaction { /** @var string */ - private $accountBic; + public $accountBic; /** @var string */ - private $accountIban; + public $accountIban; /** @var int */ - private $accountId; + public $accountId; /** @var string */ - private $accountName; + public $accountName; /** @var string */ - private $accountNumber; + public $accountNumber; /** @var string */ - private $amount; + public $amount; /** @var string */ - private $amountCredit; + public $amountCredit; /** @var string */ - private $amountDebit; + public $amountDebit; /** @var int */ - private $billId; + public $billId; /** @var string */ - private $billName; + public $billName; /** @var int */ - private $budgetId; + public $budgetId; /** @var string */ - private $budgetName; + public $budgetName; /** @var int */ - private $categoryId; + public $categoryId; /** @var string */ - private $categoryName; + public $categoryName; /** @var string */ - private $currencyCode; + public $currencyCode; /** @var int */ - private $currencyId; + public $currencyId; /** @var string */ - private $currencyName; + public $currencyName; /** @var string */ - private $currencySymbol; + public $currencySymbol; /** @var string */ - private $date; + public $date; /** @var string */ - private $description; + public $description; /** @var string */ - private $externalId; + public $externalId; /** @var string */ - private $foreignAmount; + public $foreignAmount; /** @var string */ - private $foreignCurrencyCode; + public $foreignCurrencyCode; /** @var int */ - private $foreignCurrencyId; + public $foreignCurrencyId; /** @var array */ - private $meta; + public $meta; /** @var array */ - private $modifiers; + public $modifiers; /** @var string */ - private $note; + public $note; /** @var string */ - private $opposingBic; + public $opposingBic; /** @var string */ - private $opposingIban; + public $opposingIban; /** @var int */ - private $opposingId; + public $opposingId; /** @var string */ - private $opposingName; + public $opposingName; /** @var string */ - private $opposingNumber; + public $opposingNumber; /** @var array */ - private $tags; + public $tags; /** * ImportTransaction constructor. @@ -133,9 +133,11 @@ class ImportTransaction { switch ($columnValue->getRole()) { default: + // @codeCoverageIgnoreStart throw new FireflyException( sprintf('ImportTransaction cannot handle role "%s" with value "%s"', $columnValue->getRole(), $columnValue->getValue()) ); + // @codeCoverageIgnoreEnd case 'account-id': // could be the result of a mapping? $this->accountId = $this->getMappedValue($columnValue); @@ -152,10 +154,10 @@ class ImportTransaction case 'account-number': $this->accountNumber = $columnValue->getValue(); break; - case'amount_debit': + case 'amount_debit': $this->amountDebit = $columnValue->getValue(); break; - case'amount_credit': + case 'amount_credit': $this->amountCredit = $columnValue->getValue(); break; case 'amount': @@ -225,10 +227,10 @@ class ImportTransaction $this->date = $columnValue->getValue(); break; case 'description': - $this->description .= $columnValue->getValue(); + $this->description = trim($this->description . ' ' . $columnValue->getValue()); break; case 'note': - $this->note .= $columnValue->getValue(); + $this->note = trim($this->note . ' ' . $columnValue->getValue()); break; case 'opposing-id': @@ -246,19 +248,17 @@ class ImportTransaction case 'opposing-number': $this->opposingNumber = $columnValue->getValue(); break; - case 'rabo-debit-credit': case 'ing-debit-credit': $this->modifiers[$columnValue->getRole()] = $columnValue->getValue(); break; - case 'tags-comma': - // todo split using pre-processor. - $this->tags = $columnValue->getValue(); + $tags = explode(',', $columnValue->getValue()); + $this->tags = array_unique(array_merge($this->tags, $tags)); break; case 'tags-space': - // todo split using pre-processor. - $this->tags = $columnValue->getValue(); + $tags = explode(' ', $columnValue->getValue()); + $this->tags = array_unique(array_merge($this->tags, $tags)); break; case '_ignore': break; @@ -275,13 +275,7 @@ class ImportTransaction public function calculateAmount(): string { Log::debug('Now in importTransaction->calculateAmount()'); - $info = $this->selectAmountInput(); - - if (0 === \count($info)) { - Log::error('No amount information for this row.'); - - return ''; - } + $info = $this->selectAmountInput(); $class = $info['class'] ?? ''; if ('' === $class) { Log::error('No amount information (conversion class) for this row.'); @@ -331,6 +325,7 @@ class ImportTransaction { if (null === $this->foreignAmount) { Log::debug('ImportTransaction holds no foreign amount info.'); + return ''; } /** @var ConverterInterface $amountConverter */ @@ -364,6 +359,7 @@ class ImportTransaction /** * This array is being used to map the account the user is using. * + * @codeCoverageIgnore * @return array */ public function getAccountData(): array @@ -377,62 +373,7 @@ class ImportTransaction } /** - * @return int - */ - public function getAccountId(): int - { - return $this->accountId; - } - - /** - * @return int - */ - public function getBillId(): int - { - return $this->billId; - } - - /** - * @return null|string - */ - public function getBillName(): ?string - { - return $this->billName; - } - - /** - * @return int - */ - public function getBudgetId(): int - { - return $this->budgetId; - } - - /** - * @return string|null - */ - public function getBudgetName(): ?string - { - return $this->budgetName; - } - - /** - * @return int - */ - public function getCategoryId(): int - { - return $this->categoryId; - } - - /** - * @return string|null - */ - public function getCategoryName(): ?string - { - return $this->categoryName; - } - - /** + * @codeCoverageIgnore * @return array */ public function getCurrencyData(): array @@ -445,54 +386,7 @@ class ImportTransaction } /** - * @return int - */ - public function getCurrencyId(): int - { - return $this->currencyId; - } - - /** - * @return string - */ - public function getDate(): string - { - return $this->date; - } - - /** - * @return string - */ - public function getDescription(): string - { - return $this->description; - } - - /** - * @return int - */ - public function getForeignCurrencyId(): int - { - return $this->foreignCurrencyId; - } - - /** - * @return array - */ - public function getMeta(): array - { - return $this->meta; - } - - /** - * @return string - */ - public function getNote(): string - { - return $this->note; - } - - /** + * @codeCoverageIgnore * @return array */ public function getOpposingAccountData(): array @@ -505,25 +399,6 @@ class ImportTransaction ]; } - /** - * @return int - */ - public function getOpposingId(): int - { - return $this->opposingId; - } - - /** - * @return array - */ - public function getTags(): array - { - return []; - - // todo make sure this is an array - return $this->tags; - } - /** * Returns the mapped value if it exists in the ColumnValue object. * diff --git a/app/Support/Import/Routine/File/CSVProcessor.php b/app/Support/Import/Routine/File/CSVProcessor.php index ec924af90d..53162147c1 100644 --- a/app/Support/Import/Routine/File/CSVProcessor.php +++ b/app/Support/Import/Routine/File/CSVProcessor.php @@ -29,7 +29,6 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Attachment; use FireflyIII\Models\ImportJob; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -40,10 +39,6 @@ use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Support\Import\Placeholder\ColumnValue; use FireflyIII\Support\Import\Placeholder\ImportTransaction; -use Illuminate\Support\Collection; -use League\Csv\Exception; -use League\Csv\Reader; -use League\Csv\Statement; use Log; @@ -81,23 +76,40 @@ class CSVProcessor implements FileProcessorInterface { Log::debug('Now in CSVProcessor() run'); - // in order to actually map we also need to read the FULL file. - try { - $reader = $this->getReader(); - } catch (Exception $e) { - Log::error($e->getMessage()); - throw new FireflyException('Cannot get reader: ' . $e->getMessage()); - } - // get all lines from file: - $lines = $this->getLines($reader); + // create separate objects to handle separate tasks: + /** @var LineReader $lineReader */ + $lineReader = app(LineReader::class); + $lineReader->setImportJob($this->importJob); + $lines = $lineReader->getLines(); + // convert each line into a small set of "ColumnValue" objects, + // joining each with its mapped counterpart. + /** @var MappingConverger $mappingConverger */ + $mappingConverger = app(MappingConverger::class); + $mappingConverger->setImportJob($this->importJob); + $converged = $mappingConverger->converge($lines); + + // validate mapped values: + /** @var MappedValuesValidator $validator */ + $validator = app(MappedValuesValidator::class); + $mappedValues = $validator->validate($mappingConverger->getMappedValues()); + + // make import transaction things from these objects. + /** @var ImportableCreator $creator */ + $creator = app(ImportableCreator::class); + $importables = $creator->convertSets($converged); + + // todo parse importables from $importables and $mappedValues + + + // from here. // make import objects, according to their role: - $importables = $this->processLines($lines); + //$importables = $this->processLines($lines); // now validate all mapped values: - $this->validateMappedValues(); + //$this->validateMappedValues(); - return $this->parseImportables($importables); + //return $this->parseImportables($importables); } /** @@ -119,57 +131,6 @@ class CSVProcessor implements FileProcessorInterface } - /** - * Returns all lines from the CSV file. - * - * @param Reader $reader - * - * @return array - * @throws FireflyException - */ - private function getLines(Reader $reader): array - { - Log::debug('now in getLines()'); - $offset = isset($this->config['has-headers']) && $this->config['has-headers'] === true ? 1 : 0; - try { - $stmt = (new Statement)->offset($offset); - } catch (Exception $e) { - throw new FireflyException(sprintf('Could not execute statement: %s', $e->getMessage())); - } - $results = $stmt->process($reader); - $lines = []; - foreach ($results as $line) { - $lines[] = array_values($line); - } - - return $lines; - } - - /** - * Return an instance of a CSV file reader so content of the file can be read. - * - * @throws \League\Csv\Exception - */ - private function getReader(): Reader - { - Log::debug('Now in getReader()'); - $content = ''; - /** @var Collection $collection */ - $collection = $this->importJob->attachments; - /** @var Attachment $attachment */ - foreach ($collection as $attachment) { - if ($attachment->filename === 'import_file') { - $content = $this->attachments->getAttachmentContent($attachment); - break; - } - } - $config = $this->repository->getConfiguration($this->importJob); - $reader = Reader::createFromString($content); - $reader->setDelimiter($config['delimiter']); - - return $reader; - } - /** * If the value in the column is mapped to a certain ID, * the column where this ID must be placed will change. @@ -496,9 +457,9 @@ class CSVProcessor implements FileProcessorInterface $opposingId = $this->verifyObjectId('opposing-id', $importable->getOpposingId()); // also needs amount to be final. //$account = $this->mapAccount($accountId, $importable->getAccountData()); - $source = $this->mapAssetAccount($accountId, $importable->getAccountData()); - $destination = $this->mapOpposingAccount($opposingId, $amount, $importable->getOpposingAccountData()); - $currency = $this->mapCurrency($currencyId, $importable->getCurrencyData()); + $source = $this->mapAssetAccount($accountId, $importable->getAccountData()); + $destination = $this->mapOpposingAccount($opposingId, $amount, $importable->getOpposingAccountData()); + $currency = $this->mapCurrency($currencyId, $importable->getCurrencyData()); $foreignCurrency = $this->mapCurrency($foreignCurrencyId, $importable->getForeignCurrencyData()); if (null === $currency) { Log::debug(sprintf('Could not map currency, use default (%s)', $this->defaultCurrency->code)); @@ -595,129 +556,6 @@ class CSVProcessor implements FileProcessorInterface } - /** - * Process all lines in the CSV file. Each line is processed separately. - * - * @param array $lines - * - * @return array - * @throws FireflyException - */ - private function processLines(array $lines): array - { - Log::debug('Now in processLines()'); - $processed = []; - $count = \count($lines); - foreach ($lines as $index => $line) { - Log::debug(sprintf('Now at line #%d of #%d', $index, $count)); - $processed[] = $this->processSingleLine($line); - } - - return $processed; - } - - /** - * Process a single line in the CSV file. - * Each column is processed separately. - * - * @param array $line - * @param array $roles - * - * @return ImportTransaction - * @throws FireflyException - */ - private function processSingleLine(array $line): ImportTransaction - { - Log::debug('Now in processSingleLine()'); - $transaction = new ImportTransaction; - // todo run all specifics on row. - foreach ($line as $column => $value) { - - $value = trim($value); - $originalRole = $this->config['column-roles'][$column] ?? '_ignore'; - Log::debug(sprintf('Now at column #%d (%s), value "%s"', $column, $originalRole, $value)); - if ($originalRole !== '_ignore' && \strlen($value) > 0) { - - // is a mapped value present? - $mapped = $this->config['column-mapping-config'][$column][$value] ?? 0; - // the role might change. - $role = $this->getRoleForColumn($column, $mapped); - - $columnValue = new ColumnValue; - $columnValue->setValue($value); - $columnValue->setRole($role); - $columnValue->setMappedValue($mapped); - $columnValue->setOriginalRole($originalRole); - $transaction->addColumnValue($columnValue); - } - if ('' === $value) { - Log::debug('Column skipped because value is empty.'); - } - } - - return $transaction; - } - - /** - * For each value that has been mapped, this method will check if the mapped value(s) are actually existing - * - * User may indicate that he wants his categories mapped to category #3, #4, #5 but if #5 is owned by another - * user it will be removed. - * - * @throws FireflyException - */ - private function validateMappedValues() - { - Log::debug('Now in validateMappedValues()'); - foreach ($this->mappedValues as $role => $values) { - $values = array_unique($values); - if (count($values) > 0) { - switch ($role) { - default: - throw new FireflyException(sprintf('Cannot validate mapped values for role "%s"', $role)); - case 'opposing-id': - case 'account-id': - $set = $this->accountRepos->getAccountsById($values); - $valid = $set->pluck('id')->toArray(); - $this->mappedValues[$role] = $valid; - break; - case 'currency-id': - case 'foreign-currency-id': - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $set = $repository->getByIds($values); - $valid = $set->pluck('id')->toArray(); - $this->mappedValues[$role] = $valid; - break; - case 'bill-id': - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $set = $repository->getByIds($values); - $valid = $set->pluck('id')->toArray(); - $this->mappedValues[$role] = $valid; - break; - case 'budget-id': - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $set = $repository->getByIds($values); - $valid = $set->pluck('id')->toArray(); - $this->mappedValues[$role] = $valid; - break; - case 'category-id': - /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $set = $repository->getByIds($values); - $valid = $set->pluck('id')->toArray(); - $this->mappedValues[$role] = $valid; - break; - } - } - } - } /** * A small function that verifies if this particular key (ID) is present in the list diff --git a/app/Support/Import/Routine/File/ImportableCreator.php b/app/Support/Import/Routine/File/ImportableCreator.php new file mode 100644 index 0000000000..563e558e1c --- /dev/null +++ b/app/Support/Import/Routine/File/ImportableCreator.php @@ -0,0 +1,70 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\File; + +use FireflyIII\Support\Import\Placeholder\ColumnValue; +use FireflyIII\Support\Import\Placeholder\ImportTransaction; + +/** + * Takes an array of arrays of ColumnValue objects and returns one (1) ImportTransaction + * for each line. + * + * Class ImportableCreator + */ +class ImportableCreator +{ + /** + * @param array $sets + * + * @return array + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function convertSets(array $sets): array + { + $return = []; + foreach ($sets as $set) { + $return[] = $this->convertSet($set); + } + + return $return; + } + + /** + * @param array $set + * + * @return ImportTransaction + * @throws \FireflyIII\Exceptions\FireflyException + */ + private function convertSet(array $set): ImportTransaction + { + $transaction = new ImportTransaction; + /** @var ColumnValue $entry */ + foreach ($set as $entry) { + $transaction->addColumnValue($entry); + } + + return $transaction; + } + +} \ No newline at end of file diff --git a/app/Support/Import/Routine/File/LineReader.php b/app/Support/Import/Routine/File/LineReader.php new file mode 100644 index 0000000000..36744c5d38 --- /dev/null +++ b/app/Support/Import/Routine/File/LineReader.php @@ -0,0 +1,173 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\File; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Import\Specifics\SpecificInterface; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use League\Csv\Reader; +use League\Csv\Statement; +use Log; + +/** + * Class LineReader + */ +class LineReader +{ + /** @var AttachmentHelperInterface */ + private $attachments; + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Grab all lines from the import job. + * + * @return array + * @throws FireflyException + */ + public function getLines(): array + { + try { + $reader = $this->getReader(); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + Log::error($e->getMessage()); + throw new FireflyException('Cannot get reader: ' . $e->getMessage()); + } + // @codeCoverageIgnoreEnd + // get all lines from file: + $lines = $this->getAllLines($reader); + + // apply specifics and return. + return $this->applySpecifics($lines); + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->attachments = app(AttachmentHelperInterface::class); + $this->repository->setUser($importJob->user); + } + + /** + * @param array $lines + * + * @return array + */ + private function applySpecifics(array $lines): array + { + $config = $this->importJob->configuration; + $validSpecifics = array_keys(config('csv.import_specifics')); + $specifics = $config['specifics'] ?? []; + $names = array_keys($specifics); + $toApply = []; + foreach ($names as $name) { + if (!\in_array($name, $validSpecifics, true)) { + continue; + } + $class = config(sprintf('csv.import_specifics.%s', $name)); + $toApply[] = app($class); + } + $return = []; + /** @var array $line */ + foreach ($lines as $line) { + /** @var SpecificInterface $specific */ + foreach ($toApply as $specific) { + $line = $specific->run($line); + } + $return[] = $line; + } + + return $return; + } + + /** + * @param Reader $reader + * + * @return array + * @throws FireflyException + */ + private function getAllLines(Reader $reader): array + { + /** @var array $config */ + $config = $this->importJob->configuration; + Log::debug('now in getLines()'); + $offset = isset($config['has-headers']) && $config['has-headers'] === true ? 1 : 0; + try { + $stmt = (new Statement)->offset($offset); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + throw new FireflyException(sprintf('Could not execute statement: %s', $e->getMessage())); + } + // @codeCoverageIgnoreEnd + $results = $stmt->process($reader); + $lines = []; + foreach ($results as $line) { + $lines[] = array_values($line); + } + + return $lines; + } + + /** + * @return Reader + * @throws FireflyException + */ + private function getReader(): Reader + { + Log::debug('Now in getReader()'); + $content = ''; + $collection = $this->repository->getAttachments($this->importJob); + /** @var Attachment $attachment */ + foreach ($collection as $attachment) { + if ($attachment->filename === 'import_file') { + $content = $this->attachments->getAttachmentContent($attachment); + break; + } + } + $config = $this->repository->getConfiguration($this->importJob); + $reader = Reader::createFromString($content); + try { + $reader->setDelimiter($config['delimiter'] ?? ','); + // @codeCoverageIgnoreStart + } catch (\League\Csv\Exception $e) { + throw new FireflyException(sprintf('Cannot set delimiter: %s', $e->getMessage())); + } + // @codeCoverageIgnoreEnd + + return $reader; + } + + +} \ No newline at end of file diff --git a/app/Support/Import/Routine/File/MappedValuesValidator.php b/app/Support/Import/Routine/File/MappedValuesValidator.php new file mode 100644 index 0000000000..c2f23cb54a --- /dev/null +++ b/app/Support/Import/Routine/File/MappedValuesValidator.php @@ -0,0 +1,128 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\File; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Log; + +/** + * Class MappedValuesValidator + */ +class MappedValuesValidator +{ + /** @var AccountRepositoryInterface */ + private $accountRepos; + /** @var BillRepositoryInterface */ + private $billRepos; + /** @var BudgetRepositoryInterface */ + private $budgetRepos; + /** @var CategoryRepositoryInterface */ + private $catRepos; + /** @var CurrencyRepositoryInterface */ + private $currencyRepos; + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + + $this->repository = app(ImportJobRepositoryInterface::class); + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + $this->billRepos = app(BillRepositoryInterface::class); + $this->budgetRepos = app(BudgetRepositoryInterface::class); + $this->catRepos = app(CategoryRepositoryInterface::class); + + $this->repository->setUser($importJob->user); + $this->accountRepos->setUser($importJob->user); + $this->currencyRepos->setUser($importJob->user); + $this->billRepos->setUser($importJob->user); + $this->budgetRepos->setUser($importJob->user); + $this->catRepos->setUser($importJob->user); + } + + + /** + * @param array $mappings + * + * @return array + * @throws FireflyException + */ + public function validate(array $mappings): array + { + $return = []; + Log::debug('Now in validateMappedValues()'); + foreach ($mappings as $role => $values) { + $values = array_unique($values); + if (\count($values) > 0) { + switch ($role) { + default: + throw new FireflyException(sprintf('Cannot validate mapped values for role "%s"', $role)); + case 'opposing-id': + case 'account-id': + $set = $this->accountRepos->getAccountsById($values); + $valid = $set->pluck('id')->toArray(); + $return[$role] = $valid; + break; + case 'currency-id': + case 'foreign-currency-id': + $set = $this->currencyRepos->getByIds($values); + $valid = $set->pluck('id')->toArray(); + $return[$role] = $valid; + break; + case 'bill-id': + $set = $this->billRepos->getByIds($values); + $valid = $set->pluck('id')->toArray(); + $return[$role] = $valid; + break; + case 'budget-id': + $set = $this->budgetRepos->getByIds($values); + $valid = $set->pluck('id')->toArray(); + $return[$role] = $valid; + break; + case 'category-id': + $set = $this->catRepos->getByIds($values); + $valid = $set->pluck('id')->toArray(); + $return[$role] = $valid; + break; + } + } + } + + return $return; + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/File/MappingConverger.php b/app/Support/Import/Routine/File/MappingConverger.php new file mode 100644 index 0000000000..12a52a5279 --- /dev/null +++ b/app/Support/Import/Routine/File/MappingConverger.php @@ -0,0 +1,213 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\File; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Import\Placeholder\ColumnValue; +use Log; + +/** + * Class MappingConverger + */ +class MappingConverger +{ + /** @var array */ + private $doMapping; + /** @var ImportJob */ + private $importJob; + /** @var array */ + private $mappedValues; + /** @var array */ + private $mapping; + /** @var ImportJobRepositoryInterface */ + private $repository; + /** @var array */ + private $roles; + + /** + * Each cell in the CSV file could be linked to a mapped value. This depends on the role of + * the column and the content of the cell. This method goes over all cells, and using their + * associated role, will see if it has been linked to a mapped value. These mapped values + * are all IDs of objects in the Firefly III database. + * + * If such a mapping exists the role of the cell changes to whatever the mapped value is. + * + * Examples: + * + * - Cell with content "Checking Account" and role "account-name". Mapping links "Checking Account" to account-id 2. + * - Cell with content "Checking Account" and role "description". No mapping, so value and role remains the same. + * + * @param array $lines + * + * @return array + * @throws FireflyException + */ + public function converge(array $lines): array + { + Log::debug('Start converging process.'); + $collection = []; + $total = \count($lines); + /** @var array $line */ + foreach ($lines as $lineIndex => $line) { + Log::debug(sprintf('Now converging line %d out of %d.', $lineIndex + 1, $total)); + $set = $this->processLine($line); + $collection[] = $set; + } + + return $collection; + + } + + /** + * @return array + */ + public function getMappedValues(): array + { + return $this->mappedValues; + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + $this->mappedValues = []; + $config = $importJob->configuration; + $this->roles = $config['column-roles'] ?? []; + $this->mapping = $config['column-mapping-config'] ?? []; + $this->doMapping = $config['column-do-mapping'] ?? []; + } + + /** + * If the value in the column is mapped to a certain ID, + * the column where this ID must be placed will change. + * + * For example, if you map role "budget-name" with value "groceries" to 1, + * then that should become the budget-id. Not the name. + * + * @param int $column + * @param int $mapped + * + * @return string + * @throws FireflyException + */ + private function getRoleForColumn(int $column, int $mapped): string + { + $role = $this->roles[$column] ?? '_ignore'; + if ($mapped === 0) { + Log::debug(sprintf('Column #%d with role "%s" is not mapped.', $column, $role)); + + return $role; + } + if (!(isset($this->doMapping[$column]) && $this->doMapping[$column] === true)) { + + return $role; + } + switch ($role) { + default: + throw new FireflyException(sprintf('Cannot indicate new role for mapped role "%s"', $role)); // @codeCoverageIgnore + case 'account-id': + case 'account-name': + case 'account-iban': + case 'account-number': + $newRole = 'account-id'; + break; + case 'bill-id': + case 'bill-name': + $newRole = 'bill-id'; + break; + case 'budget-id': + case 'budget-name': + $newRole = 'budget-id'; + break; + case 'currency-id': + case 'currency-name': + case 'currency-code': + case 'currency-symbol': + $newRole = 'currency-id'; + break; + case 'category-id': + case 'category-name': + $newRole = 'category-id'; + break; + case 'foreign-currency-id': + case 'foreign-currency-code': + $newRole = 'foreign-currency-id'; + break; + case 'opposing-id': + case 'opposing-name': + case 'opposing-iban': + case 'opposing-number': + $newRole = 'opposing-id'; + break; + } + Log::debug(sprintf('Role was "%s", but because of mapping, role becomes "%s"', $role, $newRole)); + + // also store the $mapped values in a "mappedValues" array. + $this->mappedValues[$newRole][] = $mapped; + + return $newRole; + } + + /** + * @param array $line + * + * @return array + * @throws FireflyException + */ + private function processLine(array $line): array + { + $return = []; + foreach ($line as $columnIndex => $value) { + $value = trim($value); + $originalRole = $this->roles[$columnIndex] ?? '_ignore'; + Log::debug(sprintf('Now at column #%d (%s), value "%s"', $columnIndex, $originalRole, $value)); + if ($originalRole !== '_ignore' && \strlen($value) > 0) { + + // is a mapped value present? + $mapped = $this->mapping[$columnIndex][$value] ?? 0; + // the role might change. + $role = $this->getRoleForColumn($columnIndex, $mapped); + + $columnValue = new ColumnValue; + $columnValue->setValue($value); + $columnValue->setRole($role); + $columnValue->setMappedValue($mapped); + $columnValue->setOriginalRole($originalRole); + $return[] = $columnValue; + } + if ('' === $value) { + Log::debug('Column skipped because value is empty.'); + } + } + + return $return; + } + +} \ No newline at end of file