. */ declare(strict_types=1); namespace FireflyIII\Support\Import\Configuration\File; use FireflyIII\Import\Specifics\SpecificInterface; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Support\Import\Configuration\ConfigurationInterface; use League\Csv\Reader; use League\Csv\Statement; use Log; /** * Class Roles. */ class Roles implements ConfigurationInterface { /** * @var array */ private $data = []; /** @var ImportJob */ private $job; /** @var ImportJobRepositoryInterface */ private $repository; /** @var string */ private $warning = ''; /** * Get the data necessary to show the configuration screen. * * @return array * * @throws \League\Csv\Exception */ public function getData(): array { $content = $this->repository->uploadFileContents($this->job); $config = $this->getConfig(); $headers = []; $offset = 0; // create CSV reader. $reader = Reader::createFromString($content); $reader->setDelimiter($config['delimiter']); // CSV headers. Ignore reader. Simply get the first row. if ($config['has-headers']) { $offset = 1; $stmt = (new Statement)->limit(1)->offset(0); $records = $stmt->process($reader); $headers = $records->fetchOne(0); Log::debug('Detected file headers:', $headers); } // example rows: $stmt = (new Statement)->limit((int)config('csv.example_rows', 5))->offset($offset); // set data: $roles = $this->getRoles(); asort($roles); $this->data = [ 'examples' => [], 'roles' => $roles, 'total' => 0, 'headers' => $headers, ]; $records = $stmt->process($reader); foreach ($records as $row) { $row = array_values($row); $row = $this->processSpecifics($row); $count = count($row); $this->data['total'] = $count > $this->data['total'] ? $count : $this->data['total']; $this->processRow($row); } $this->updateColumCount(); $this->makeExamplesUnique(); return $this->data; } /** * Return possible warning to user. * * @return string */ public function getWarningMessage(): string { return $this->warning; } /** * @param ImportJob $job * * @return ConfigurationInterface */ public function setJob(ImportJob $job): ConfigurationInterface { $this->job = $job; $this->repository = app(ImportJobRepositoryInterface::class); $this->repository->setUser($job->user); return $this; } /** * Store the result. * * @param array $data * * @return bool */ public function storeConfiguration(array $data): bool { Log::debug('Now in storeConfiguration of Roles.'); $config = $this->getConfig(); $count = $config['column-count']; for ($i = 0; $i < $count; ++$i) { $role = $data['role'][$i] ?? '_ignore'; $mapping = isset($data['map'][$i]) && $data['map'][$i] === '1' ? true : false; $config['column-roles'][$i] = $role; $config['column-do-mapping'][$i] = $mapping; Log::debug(sprintf('Column %d has been given role %s (mapping: %s)', $i, $role, var_export($mapping, true))); } $this->saveConfig($config); $this->ignoreUnmappableColumns(); $res = $this->isRolesComplete(); if ($res === true) { $config = $this->getConfig(); $config['stage'] = 'map'; $this->saveConfig($config); $this->isMappingNecessary(); } return true; } /** * Short hand method. * * @return array */ private function getConfig(): array { return $this->repository->getConfiguration($this->job); } /** * @return array */ private function getRoles(): array { $roles = []; foreach (array_keys(config('csv.import_roles')) as $role) { $roles[$role] = trans('import.column_' . $role); } return $roles; } /** * @return bool */ private function ignoreUnmappableColumns(): bool { Log::debug('Now in ignoreUnmappableColumns()'); $config = $this->getConfig(); $count = $config['column-count']; for ($i = 0; $i < $count; ++$i) { $role = $config['column-roles'][$i] ?? '_ignore'; $mapping = $config['column-do-mapping'][$i] ?? false; Log::debug(sprintf('Role for column %d is %s, and mapping is %s', $i, $role, var_export($mapping, true))); if ('_ignore' === $role && true === $mapping) { $mapping = false; Log::debug(sprintf('Column %d has type %s so it cannot be mapped.', $i, $role)); } $config['column-do-mapping'][$i] = $mapping; } $this->saveConfig($config); return true; } /** * @return bool */ private function isMappingNecessary() { $config = $this->getConfig(); $count = $config['column-count']; $toBeMapped = 0; for ($i = 0; $i < $count; ++$i) { $mapping = $config['column-do-mapping'][$i] ?? false; if (true === $mapping) { ++$toBeMapped; } } Log::debug(sprintf('Found %d columns that need mapping.', $toBeMapped)); if (0 === $toBeMapped) { $config['stage'] = 'ready'; } $this->saveConfig($config); return true; } /** * @return bool */ private function isRolesComplete(): bool { $config = $this->getConfig(); $count = $config['column-count']; $assigned = 0; // check if data actually contains amount column (foreign amount does not count) $hasAmount = false; $hasForeignAmount = false; $hasForeignCode = false; for ($i = 0; $i < $count; ++$i) { $role = $config['column-roles'][$i] ?? '_ignore'; if ('_ignore' !== $role) { ++$assigned; } if (in_array($role, ['amount', 'amount_credit', 'amount_debit'])) { $hasAmount = true; } if ($role === 'foreign-currency-code') { $hasForeignCode = true; } if ($role === 'amount_foreign') { $hasForeignAmount = true; } } Log::debug( sprintf( 'Assigned is %d, hasAmount %s, hasForeignCode %s, hasForeignAmount %s', $assigned, var_export($hasAmount, true), var_export($hasForeignCode, true), var_export($hasForeignAmount, true) ) ); // all assigned and correct foreign info if ($assigned > 0 && $hasAmount && ($hasForeignCode === $hasForeignAmount)) { $this->warning = ''; $this->saveConfig($config); Log::debug('isRolesComplete() returns true.'); return true; } // warn if has foreign amount but no currency code: if ($hasForeignAmount && !$hasForeignCode) { $this->warning = (string)trans('import.foreign_amount_warning'); Log::debug('isRolesComplete() returns FALSE because foreign amount present without foreign code.'); return false; } if (0 === $assigned || !$hasAmount) { $this->warning = (string)trans('import.roles_warning'); Log::debug('isRolesComplete() returns FALSE because no amount present.'); return false; } Log::debug('isRolesComplete() returns FALSE because no reason.'); return false; } /** * make unique example data. */ private function makeExamplesUnique(): bool { foreach ($this->data['examples'] as $index => $values) { $this->data['examples'][$index] = array_unique($values); } return true; } /** * @param array $row * * @return bool */ private function processRow(array $row): bool { foreach ($row as $index => $value) { $value = trim($value); if (strlen($value) > 0) { $this->data['examples'][$index][] = $value; } } return true; } /** * run specifics here: * and this is the point where the specifix go to work. * * @param array $row * * @return array */ private function processSpecifics(array $row): array { $config = $this->getConfig(); $specifics = $config['specifics'] ?? []; $names = array_keys($specifics); foreach ($names as $name) { /** @var SpecificInterface $specific */ $specific = app('FireflyIII\Import\Specifics\\' . $name); $row = $specific->run($row); } return $row; } /** * @param array $array */ private function saveConfig(array $array) { $this->repository->setConfiguration($this->job, $array); } /** * @return bool */ private function updateColumCount(): bool { $config = $this->getConfig(); $count = $this->data['total']; $config['column-count'] = $count; $this->saveConfig($config); return true; } }