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.

254 lines
8.5 KiB

4 years ago
  1. <?php
  2. /*
  3. * RoleService.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\Roles;
  24. use App\Services\Camt\Transaction;
  25. use App\Services\Session\Constants;
  26. use App\Services\Shared\Configuration\Configuration;
  27. use App\Services\Storage\StorageService;
  28. use Genkgo\Camt\Camt053\DTO\Statement as CamtStatement;
  29. use Genkgo\Camt\Config;
  30. use Genkgo\Camt\Reader as CamtReader;
  31. use Illuminate\Support\Facades\Log;
  32. use League\Csv\Exception;
  33. use League\Csv\InvalidArgument;
  34. use League\Csv\Reader;
  35. use League\Csv\Statement;
  36. use League\Csv\UnableToProcessCsv;
  37. /**
  38. * Class RoleService
  39. */
  40. class RoleService
  41. {
  42. public const EXAMPLE_COUNT = 7;
  43. public const EXAMPLE_LENGTH = 26;
  44. /**
  45. * @throws InvalidArgument
  46. * @throws UnableToProcessCsv
  47. */
  48. public static function getColumns(string $content, Configuration $configuration): array
  49. {
  50. $reader = Reader::createFromString($content);
  51. // configure reader:
  52. $delimiter = $configuration->getDelimiter();
  53. switch ($delimiter) {
  54. default:
  55. case 'comma':
  56. $reader->setDelimiter(',');
  57. break;
  58. case 'semicolon':
  59. $reader->setDelimiter(';');
  60. break;
  61. case 'tab':
  62. $reader->setDelimiter("\t");
  63. break;
  64. }
  65. $headers = [];
  66. if (true === $configuration->isHeaders()) {
  67. try {
  68. $stmt = new Statement()->limit(1)->offset(0);
  69. $records = $stmt->process($reader);
  70. $headers = $records->fetchOne();
  71. // @codeCoverageIgnoreStart
  72. } catch (Exception $e) {
  73. Log::error($e->getMessage());
  74. throw new \InvalidArgumentException($e->getMessage());
  75. }
  76. // @codeCoverageIgnoreEnd
  77. Log::debug('Detected file headers:', $headers);
  78. }
  79. if (false === $configuration->isHeaders()) {
  80. Log::debug('Role service: file has no headers');
  81. try {
  82. $stmt = new Statement()->limit(1)->offset(0);
  83. $records = $stmt->process($reader);
  84. $count = count($records->fetchOne());
  85. Log::debug(sprintf('Role service: first row has %d columns', $count));
  86. for ($i = 0; $i < $count; ++$i) {
  87. $headers[] = sprintf('Column #%d', $i + 1);
  88. }
  89. // @codeCoverageIgnoreStart
  90. } catch (Exception $e) {
  91. Log::error($e->getMessage());
  92. throw new \InvalidArgumentException($e->getMessage());
  93. }
  94. }
  95. return $headers;
  96. }
  97. /**
  98. * @throws Exception
  99. */
  100. public static function getExampleData(string $content, Configuration $configuration): array
  101. {
  102. $reader = Reader::createFromString($content);
  103. // configure reader:
  104. $delimiter = $configuration->getDelimiter();
  105. switch ($delimiter) {
  106. default:
  107. case 'comma':
  108. $reader->setDelimiter(',');
  109. break;
  110. case 'semicolon':
  111. $reader->setDelimiter(';');
  112. break;
  113. case 'tab':
  114. $reader->setDelimiter("\t");
  115. break;
  116. }
  117. $offset = $configuration->isHeaders() ? 1 : 0;
  118. $examples = [];
  119. // make statement.
  120. try {
  121. $stmt = new Statement()->limit(self::EXAMPLE_COUNT)->offset($offset);
  122. // @codeCoverageIgnoreStart
  123. } catch (Exception $e) {
  124. Log::error($e->getMessage());
  125. throw new \InvalidArgumentException($e->getMessage());
  126. }
  127. /** @codeCoverageIgnoreEnd */
  128. // grab the records:
  129. $records = $stmt->process($reader);
  130. /** @var array $line */
  131. foreach ($records as $line) {
  132. $line = array_values($line);
  133. // $line = SpecificService::runSpecifics($line, $configuration->getSpecifics());
  134. foreach ($line as $index => $cell) {
  135. if (strlen((string) $cell) > self::EXAMPLE_LENGTH) {
  136. $cell = sprintf('%s...', substr((string) $cell, 0, self::EXAMPLE_LENGTH));
  137. }
  138. $examples[$index][] = $cell;
  139. $examples[$index] = array_unique($examples[$index]);
  140. }
  141. }
  142. foreach ($examples as $line => $entries) {
  143. asort($entries);
  144. $examples[$line] = $entries;
  145. }
  146. return $examples;
  147. }
  148. public static function getExampleDataFromCamt(string $content, Configuration $configuration): array
  149. {
  150. $camtReader = new CamtReader(Config::getDefault());
  151. $camtMessage = $camtReader->readString(StorageService::getContent(session()->get(Constants::UPLOAD_DATA_FILE))); // -> Level A
  152. $transactions = [];
  153. $examples = [];
  154. $fieldNames = array_keys(config('camt.fields'));
  155. foreach ($fieldNames as $name) {
  156. $examples[$name] = [];
  157. }
  158. /**
  159. * This code creates separate Transaction objects for transaction details,
  160. * even when the user indicates these details should be splits or ignored entirely.
  161. * This is because we still need to extract possible example data from these transaction details.
  162. */
  163. $statements = $camtMessage->getRecords();
  164. /** @var CamtStatement $statement */
  165. foreach ($statements as $statement) { // -> Level B
  166. $entries = $statement->getEntries();
  167. foreach ($entries as $entry) { // -> Level C
  168. $count = count($entry->getTransactionDetails()); // count level D entries.
  169. if (0 === $count) {
  170. // TODO Create a single transaction, I guess?
  171. $transactions[] = new Transaction($configuration, $camtMessage, $statement, $entry, []);
  172. }
  173. if (0 !== $count) {
  174. foreach ($entry->getTransactionDetails() as $detail) {
  175. $transactions[] = new Transaction($configuration, $camtMessage, $statement, $entry, [$detail]);
  176. }
  177. }
  178. }
  179. }
  180. $count = 0;
  181. /** @var Transaction $transaction */
  182. foreach ($transactions as $transaction) {
  183. if (15 === $count) { // do not check more than 15 transactions to fill the example-data
  184. break;
  185. }
  186. foreach ($fieldNames as $name) {
  187. if (array_key_exists($name, $examples)) { // there is at least one example, so we can check how many
  188. if (count($examples[$name]) > 5) { // there are already five examples, so jump to next field
  189. continue;
  190. }
  191. } // otherwise, try to fetch data
  192. $splits = $transaction->countSplits();
  193. if (0 === $splits) {
  194. $value = $transaction->getFieldByIndex($name, 0);
  195. if ('' !== $value) {
  196. $examples[$name][] = $value;
  197. }
  198. }
  199. if ($splits > 0) {
  200. for ($index = 0; $index < $splits; ++$index) {
  201. $value = $transaction->getFieldByIndex($name, $index);
  202. if ('' !== $value) {
  203. $examples[$name][] = $value;
  204. }
  205. }
  206. }
  207. }
  208. ++$count;
  209. }
  210. foreach ($examples as $key => $list) {
  211. $examples[$key] = array_unique($list);
  212. $examples[$key] = array_filter($examples[$key], fn (string $value) => '' !== $value);
  213. }
  214. return $examples;
  215. }
  216. }