You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

219 lines
8.9 KiB

  1. <?php
  2. /*
  3. * LineProcessor.php
  4. * Copyright (c) 2021 james@firefly-iii.org
  5. *
  6. * This file is part of the Firefly III Data Importer
  7. * (https://github.com/firefly-iii/data-importer).
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. */
  22. declare(strict_types=1);
  23. namespace App\Services\CSV\Conversion\Routine;
  24. use App\Exceptions\ImporterErrorException;
  25. use App\Services\Shared\Configuration\Configuration;
  26. use App\Services\Shared\Conversion\ProgressInformation;
  27. use Illuminate\Support\Facades\Log;
  28. /**
  29. * Class LineProcessor
  30. *
  31. * Processes single lines from a CSV file. Converts them into
  32. * arrays with single "ColumnValue" that hold the value + the role of the column
  33. * + the mapped value (if any).
  34. */
  35. class LineProcessor
  36. {
  37. use ProgressInformation;
  38. private string $dateFormat;
  39. private array $doMapping;
  40. private array $mappedValues;
  41. private array $mapping;
  42. private array $roles;
  43. /**
  44. * LineProcessor constructor.
  45. */
  46. public function __construct(Configuration $configuration)
  47. {
  48. Log::debug('Created LineProcessor()');
  49. Log::debug('Roles', $configuration->getRoles());
  50. Log::debug('Mapping (will not be printed)');
  51. $this->roles = $configuration->getRoles();
  52. $this->mapping = $configuration->getMapping();
  53. $this->doMapping = $configuration->getDoMapping();
  54. $this->dateFormat = $configuration->getDate();
  55. }
  56. public function processCSVLines(array $lines): array
  57. {
  58. $processed = [];
  59. $count = count($lines);
  60. Log::info(sprintf('Now processing the data in the %d CSV lines...', $count));
  61. foreach ($lines as $index => $line) {
  62. Log::debug(sprintf('Now processing CSV line #%d/#%d', $index + 1, $count));
  63. try {
  64. $processed[] = $this->process($line);
  65. } catch (ImporterErrorException $e) {
  66. Log::error(sprintf('[%s]: %s', config('importer.version'), $e->getMessage()));
  67. // Log::error($e->getTraceAsString());
  68. $this->addError(0, $e->getMessage());
  69. }
  70. }
  71. Log::info(sprintf('Done processing data in %d CSV lines...', $count));
  72. return $processed;
  73. }
  74. /**
  75. * Convert each raw CSV to a set of ColumnValue objects, which hold as much info
  76. * as we can cram into it. These new lines can be imported later on.
  77. *
  78. * @throws ImporterErrorException
  79. */
  80. private function process(array $line): array
  81. {
  82. Log::debug(sprintf('[%s] Now in %s', config('importer.version'), __METHOD__));
  83. $count = count($line);
  84. $return = [];
  85. foreach ($line as $columnIndex => $value) {
  86. Log::debug(sprintf('Now at column %d/%d', $columnIndex + 1, $count));
  87. $value = trim((string)$value);
  88. $originalRole = $this->roles[$columnIndex] ?? '_ignore';
  89. Log::debug(sprintf('Now at column #%d (%s), value "%s"', $columnIndex + 1, $originalRole, $value));
  90. if ('_ignore' === $originalRole) {
  91. Log::debug(sprintf('Ignore column #%d because role is "_ignore".', $columnIndex + 1));
  92. continue;
  93. }
  94. if ('' === $value) {
  95. Log::debug(sprintf('Ignore column #%d because value is "".', $columnIndex + 1));
  96. continue;
  97. }
  98. // is a mapped value present?
  99. $mapped = $this->mapping[$columnIndex][$value] ?? 0;
  100. Log::debug(sprintf('ColumnIndex is %s', var_export($columnIndex, true)));
  101. Log::debug(sprintf('Value is %s', var_export($value, true)));
  102. // Log::debug('Local mapping (will not be printed)');
  103. // the role might change because of the mapping.
  104. $role = $this->getRoleForColumn($columnIndex, $mapped);
  105. $appendValue = config(sprintf('csv.import_roles.%s.append_value', $originalRole));
  106. if (null === $appendValue) {
  107. $appendValue = false;
  108. }
  109. // Log::debug(sprintf('Append value config: %s', sprintf('csv.import_roles.%s.append_value', $originalRole)));
  110. $columnValue = new ColumnValue();
  111. $columnValue->setValue($value);
  112. $columnValue->setRole($role);
  113. $columnValue->setAppendValue($appendValue);
  114. $columnValue->setMappedValue($mapped);
  115. $columnValue->setOriginalRole($originalRole);
  116. // if column role is 'date', add the date config for conversion:
  117. if (in_array($originalRole, ['date_transaction', 'date_interest', 'date_due', 'date_payment', 'date_process', 'date_book', 'date_invoice'], true)) {
  118. Log::debug(sprintf('Because role is %s, set date format to "%s" (via setConfiguration).', $originalRole, $this->dateFormat));
  119. $columnValue->setConfiguration($this->dateFormat);
  120. }
  121. $return[] = $columnValue;
  122. }
  123. // add a special column value for the "source"
  124. $columnValue = new ColumnValue();
  125. $columnValue->setValue(sprintf('jc5-data-import-v%s', config('importer.version')));
  126. $columnValue->setMappedValue(0);
  127. $columnValue->setAppendValue(false);
  128. $columnValue->setRole('original-source');
  129. $return[] = $columnValue;
  130. Log::debug(sprintf('Added column #%d to denote the original source.', count($return)));
  131. return $return;
  132. }
  133. /**
  134. * If the value in the column is mapped to a certain ID,
  135. * the column where this ID must be placed will change.
  136. *
  137. * For example, if you map role "budget-name" with value "groceries" to 1,
  138. * then that should become the budget-id. Not the name.
  139. *
  140. * @throws ImporterErrorException
  141. */
  142. private function getRoleForColumn(int $column, int $mapped): string
  143. {
  144. $role = $this->roles[$column] ?? '_ignore';
  145. if (0 === $mapped) {
  146. Log::debug(sprintf('Column #%d with role "%s" is not mapped.', $column + 1, $role));
  147. return $role;
  148. }
  149. if (!(array_key_exists($column, $this->doMapping) && true === $this->doMapping[$column])) {
  150. // if the mapping has been filled in already by a role with a higher priority,
  151. // ignore the mapping.
  152. Log::debug(sprintf('Column #%d ("%s") has something already.', $column, $role));
  153. return $role;
  154. }
  155. $roleMapping = [
  156. 'account-id' => 'account-id',
  157. 'account-name' => 'account-id',
  158. 'account-iban' => 'account-id',
  159. 'account-number' => 'account-id',
  160. 'bill-id' => 'bill-id',
  161. 'bill-name' => 'bill-id',
  162. 'budget-id' => 'budget-id',
  163. 'budget-name' => 'budget-id',
  164. 'currency-id' => 'currency-id',
  165. 'currency-name' => 'currency-id',
  166. 'currency-code' => 'currency-id',
  167. 'currency-symbol' => 'currency-id',
  168. 'category-id' => 'category-id',
  169. 'category-name' => 'category-id',
  170. 'foreign-currency-id' => 'foreign-currency-id',
  171. 'foreign-currency-code' => 'foreign-currency-id',
  172. 'opposing-id' => 'opposing-id',
  173. 'opposing-name' => 'opposing-id',
  174. 'opposing-iban' => 'opposing-id',
  175. 'opposing-number' => 'opposing-id',
  176. ];
  177. if (!array_key_exists($role, $roleMapping)) {
  178. throw new ImporterErrorException(sprintf('Cannot indicate new role for mapped role "%s"', $role)); // @codeCoverageIgnore
  179. }
  180. $newRole = $roleMapping[$role];
  181. if ($newRole !== $role) {
  182. Log::debug(sprintf('Role was "%s", but because of mapping (mapped to #%d), role becomes "%s"', $role, $mapped, $newRole));
  183. }
  184. // also store the $mapped values in a "mappedValues" array.
  185. // used to validate whatever has been set as mapping
  186. $this->mappedValues[$newRole][] = $mapped;
  187. $this->mappedValues[$newRole] = array_unique($this->mappedValues[$newRole]);
  188. Log::debug(sprintf('Values mapped to role "%s" are: ', $newRole), $this->mappedValues[$newRole]);
  189. return $newRole;
  190. }
  191. }