Browse Source

Merge pull request #860 from firefly-iii/develop

🤖 Automatically merge the PR into the main branch.
pull/861/head v1.7.1
github-actions[bot] 4 months ago
committed by GitHub
parent
commit
169a8d70e2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 65
      .ci/php-cs-fixer/composer.lock
  2. 24
      .github/workflows/release.yml
  3. 1
      app/Console/AutoImports.php
  4. 10
      app/Http/Controllers/Controller.php
  5. 5
      app/Http/Controllers/Import/ConfigurationController.php
  6. 7
      app/Http/Controllers/Import/ConversionController.php
  7. 2
      app/Http/Controllers/IndexController.php
  8. 2
      app/Http/Controllers/TokenController.php
  9. 3
      app/Services/CSV/Conversion/Routine/PseudoTransactionProcessor.php
  10. 39
      app/Services/Nordigen/Conversion/Routine/GenerateTransactions.php
  11. 11
      app/Services/Nordigen/Conversion/Routine/TransactionProcessor.php
  12. 20
      app/Services/Nordigen/Conversion/RoutineManager.php
  13. 4
      app/Services/Nordigen/Services/AccountInformationCollector.php
  14. 5
      app/Support/Internal/CollectsAccounts.php
  15. 97
      app/Support/Internal/MergesAccountLists.php
  16. 12
      changelog.md
  17. 154
      composer.lock
  18. 4
      config/importer.php
  19. 2
      readme.md
  20. 2
      resources/views/v2/components/firefly-iii-account-generic.blade.php
  21. 3
      resources/views/v2/components/importer-account-title.blade.php
  22. 12
      resources/views/v2/components/importer-account.blade.php

65
.ci/php-cs-fixer/composer.lock

@ -406,58 +406,59 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.75.0",
"version": "v3.76.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "399a128ff2fdaf4281e4e79b755693286cdf325c"
"reference": "0e3c484cef0ae9314b0f85986a36296087432c40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c",
"reference": "399a128ff2fdaf4281e4e79b755693286cdf325c",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/0e3c484cef0ae9314b0f85986a36296087432c40",
"reference": "0e3c484cef0ae9314b0f85986a36296087432c40",
"shasum": ""
},
"require": {
"clue/ndjson-react": "^1.0",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.3",
"composer/xdebug-handler": "^3.0.5",
"ext-filter": "*",
"ext-hash": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.2",
"php": "^7.4 || ^8.0",
"react/child-process": "^0.6.5",
"react/child-process": "^0.6.6",
"react/event-loop": "^1.0",
"react/promise": "^2.0 || ^3.0",
"react/promise": "^2.11 || ^3.0",
"react/socket": "^1.0",
"react/stream": "^1.0",
"sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0",
"symfony/console": "^5.4 || ^6.4 || ^7.0",
"symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
"symfony/filesystem": "^5.4 || ^6.4 || ^7.0",
"symfony/finder": "^5.4 || ^6.4 || ^7.0",
"symfony/options-resolver": "^5.4 || ^6.4 || ^7.0",
"symfony/polyfill-mbstring": "^1.31",
"symfony/polyfill-php80": "^1.31",
"symfony/polyfill-php81": "^1.31",
"symfony/process": "^5.4 || ^6.4 || ^7.2",
"symfony/stopwatch": "^5.4 || ^6.4 || ^7.0"
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
"symfony/console": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0",
"symfony/polyfill-mbstring": "^1.32",
"symfony/polyfill-php80": "^1.32",
"symfony/polyfill-php81": "^1.32",
"symfony/process": "^5.4.47 || ^6.4.20 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.6",
"infection/infection": "^0.29.14",
"justinrainbow/json-schema": "^5.3 || ^6.2",
"keradus/cli-executor": "^2.1",
"justinrainbow/json-schema": "^5.3 || ^6.4",
"keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.7",
"php-coveralls/php-coveralls": "^2.8",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12",
"symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3",
"symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3"
"phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25",
"symfony/polyfill-php84": "^1.32",
"symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1",
"symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
@ -498,7 +499,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.76.0"
},
"funding": [
{
@ -506,7 +507,7 @@
"type": "github"
}
],
"time": "2025-03-31T18:40:42+00:00"
"time": "2025-06-30T14:15:06+00:00"
},
{
"name": "psr/container",
@ -1256,16 +1257,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44"
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44",
"url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101",
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101",
"shasum": ""
},
"require": {
@ -1330,7 +1331,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.0"
"source": "https://github.com/symfony/console/tree/v7.3.1"
},
"funding": [
{
@ -1346,7 +1347,7 @@
"type": "tidelift"
}
],
"time": "2025-05-24T10:34:04+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/deprecation-contracts",

24
.github/workflows/release.yml

@ -230,6 +230,12 @@ jobs:
echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt
echo "" >> output.txt
echo ":warning: Please be careful with this pre-release, as it may not work as expected." >> output.txt
# donations!
echo '' >> output.txt
echo '### Support Firefly III' >> output.txt
echo 'Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. For more information, please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information.' >> output.txt
echo '' >> output.txt
fi
# describe a branch release
if [[ "$version" == branch* ]]; then
@ -250,16 +256,32 @@ jobs:
if [[ "develop" != "$version" ]] && [[ "$version" != branch* ]] && [[ "$version" != *alpha* ]] && [[ "$version" != *beta* ]]; then
echo 'Describe the latest release'
sudo chown -R runner:docker output.txt
# the changelog is in output.txt
mv output.txt output2.txt
touch output.txt
echo '' >> output.txt
echo "Welcome to release $version of the Firefly III Data Importer. It contains the the latest fixes, translations and features. Docker users can find this release under the \`latest\` tag." >> output.txt
echo "Welcome to release $version of the Firefly III Data Importer. This release contains the the latest fixes, translations and features. Docker users can find this release under the \`latest\` tag." >> output.txt
echo '' >> output.txt
# add changelog to file.
cat output2.txt >> output.txt
echo '' >> output.txt
rm -f output2.txt
echo '### Instructions' >> output.txt
echo '' >> output.txt
echo "* Installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt
echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt
echo "* The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/)." >> output.txt
# donations!
echo '' >> output.txt
echo '### Support Firefly III' >> output.txt
echo 'Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. For more information, please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information.' >> output.txt
echo '' >> output.txt
fi
# describe alpha release

1
app/Console/AutoImports.php

@ -174,7 +174,6 @@ trait AutoImports
{
$exitCodes = [];
// @var string $file
foreach ($files as $jsonFile => $importableFile) {
try {
$exitCodes[$importableFile] = $this->importFile($jsonFile, $importableFile);

10
app/Http/Controllers/Controller.php

@ -48,13 +48,13 @@ class Controller extends BaseController
public function __construct()
{
// validate some env vars (skip over config)
// $accessToken = (string) env('FIREFLY_III_ACCESS_TOKEN', '');
// $clientId = (string) env('FIREFLY_III_CLIENT_ID', '');
// $baseUrl = (string) env('FIREFLY_III_URL', '');
// $vanityUrl = (string) env('VANITY_URL', '');
// $accessToken = (string) env('FIREFLY_III_ACCESS_TOKEN', '');
// $clientId = (string) env('FIREFLY_III_CLIENT_ID', '');
// $baseUrl = (string) env('FIREFLY_III_URL', '');
// $vanityUrl = (string) env('VANITY_URL', '');
// experimental. Use config instead
$accessToken = (string) config('importer.access_token','');
$accessToken = (string) config('importer.access_token', '');
$clientId = (string) config('importer.client_id', '');
$baseUrl = (string) config('importer.url', '');
$vanityUrl = (string) config('importer.vanity_url', '');

5
app/Http/Controllers/Import/ConfigurationController.php

@ -98,8 +98,8 @@ class ConfigurationController extends Controller
}
// collect Firefly III accounts
// this function returns an array with keys 'assets' and 'liabilities', each containing an array of Firefly III accounts.
$fireflyIIIaccounts = $this->getFireflyIIIAccounts();
// possibilities for duplicate detection (unique columns)
// also get the nordigen / spectre accounts
@ -146,8 +146,7 @@ class ConfigurationController extends Controller
$fileType = $detector->detectContentTypeFromContent($content);
$configuration->setContentType($fileType);
}
// Get currency data for SimpleFIN account creation widget
// Get currency data for account creation widget
$currencies = $this->getCurrencies();
return view('import.004-configure.index', compact('mainTitle', 'subTitle', 'fireflyIIIaccounts', 'configuration', 'flow', 'importerAccounts', 'uniqueColumns', 'currencies'));

7
app/Http/Controllers/Import/ConversionController.php

@ -33,6 +33,7 @@ use App\Services\CSV\Conversion\RoutineManager as CSVRoutineManager;
use App\Services\Nordigen\Conversion\RoutineManager as NordigenRoutineManager;
use App\Services\Session\Constants;
use App\Services\Shared\Conversion\ConversionStatus;
use App\Services\Shared\Conversion\RoutineManagerInterface;
use App\Services\Shared\Conversion\RoutineStatusManager;
use App\Services\SimpleFIN\Conversion\RoutineManager as SimpleFINRoutineManager;
use App\Services\SimpleFIN\Validation\ConfigurationContractValidator;
@ -117,7 +118,8 @@ class ConversionController extends Controller
if (!in_array($flow, config('importer.flows'), true)) {
throw new ImporterErrorException(sprintf('Not a supported flow: "%s"', $flow));
}
// @var RoutineManagerInterface $routine
/** @var RoutineManagerInterface $routine */
if ('file' === $flow) {
$contentType = $configuration->getContentType();
if ('unknown' === $contentType || 'csv' === $contentType) {
@ -247,7 +249,8 @@ class ConversionController extends Controller
if (!in_array($flow, config('importer.flows'), true)) {
throw new ImporterErrorException(sprintf('Not a supported flow: "%s"', $flow));
}
// @var RoutineManagerInterface $routine
/** @var RoutineManagerInterface $routine */
if ('file' === $flow) {
$contentType = $configuration->getContentType();
if ('unknown' === $contentType || 'csv' === $contentType) {

2
app/Http/Controllers/IndexController.php

@ -84,7 +84,7 @@ class IndexController extends Controller
Log::debug('IndexController authentication detection', [
'client_id' => $clientId,
'url' => $url,
'access_token_config' => $accessTokenConfig,
'access_token_config' => substr($accessTokenConfig, 0, 25).'...',
'access_token_empty' => '' === $accessTokenConfig,
]);

2
app/Http/Controllers/TokenController.php

@ -162,7 +162,6 @@ class TokenController extends Controller
$infoRequest->setVerify(config('importer.connection.verify'));
$infoRequest->setTimeOut(config('importer.connection.timeout'));
Log::debug(sprintf('Now trying to authenticate with Firefly III at %s', $url));
try {
@ -171,6 +170,7 @@ class TokenController extends Controller
} catch (ApiHttpException $e) {
Log::notice(sprintf('Could NOT authenticate with Firefly III at %s', $url));
Log::error(sprintf('Could not connect to Firefly III: %s', $e->getMessage()));
Log::debug(sprintf('Using access token "%s" (limited to 25 chars if present)', substr($token, 0, 25)));
return response()->json(['result' => 'NOK', 'message' => $e->getMessage()]);
}

3
app/Services/CSV/Conversion/Routine/PseudoTransactionProcessor.php

@ -35,6 +35,7 @@ use GrumpyDictator\FFIIIApiSupport\Model\Account;
use GrumpyDictator\FFIIIApiSupport\Model\TransactionCurrency;
use GrumpyDictator\FFIIIApiSupport\Request\GetAccountRequest;
use GrumpyDictator\FFIIIApiSupport\Request\GetCurrencyRequest;
use GrumpyDictator\FFIIIApiSupport\Response\GetAccountResponse;
use GrumpyDictator\FFIIIApiSupport\Response\GetCurrencyResponse;
use Illuminate\Support\Facades\Log;
@ -84,7 +85,7 @@ class PseudoTransactionProcessor
$accountRequest->setTimeOut(config('importer.connection.timeout'));
$accountRequest->setId($accountId);
// @var GetAccountResponse $result
/** @var GetAccountResponse $result */
try {
$result = $accountRequest->get();
} catch (ApiHttpException $e) {

39
app/Services/Nordigen/Conversion/Routine/GenerateTransactions.php

@ -238,15 +238,18 @@ class GenerateTransactions
}
// #9533 add entry reference as tag or as booking date.
if ('' !== $entry->entryReference) {
if (false === Carbon::parse($entry->entryReference)->getTimestamp()) {
$transaction['tags'][] = $entry->entryReference;
$parsed = null;
try {
$parsed = Carbon::parse($entry->entryReference)->getTimestamp();
} catch (InvalidFormatException $e) {
Log::debug(sprintf('Cannot parse entry reference "%s" as date, but that\'s OK.', $entry->entryReference));
}
if (null === $parsed) {
$transaction['notes'] = trim(sprintf("%s\n\nEntry reference: %s", $transaction['notes'], $entry->entryReference));
}
if (false !== Carbon::parse($entry->entryReference)->getTimestamp()) {
try {
$transaction['booking_date'] = Carbon::parse($entry->entryReference)->toW3cString();
} catch (InvalidFormatException) {
// ignore error.
}
if (null !== $parsed) {
$transaction['booking_date'] = Carbon::parse($entry->entryReference)->toW3cString();
}
}
@ -268,7 +271,7 @@ class GenerateTransactions
$transaction['amount'] = $entry->transactionAmount;
// destination is a Nordigen account (has to be!)
$transaction['destination_id'] = (int) $this->accounts[$accountId];
$transaction['destination_id'] = (int)$this->accounts[$accountId];
Log::debug(sprintf('Destination ID is now #%d, which should be a Firefly III asset account.', $transaction['destination_id']));
// append source iban and number (if present)
@ -302,7 +305,7 @@ class GenerateTransactions
}
}
$transaction = $this->positiveTransactionSafetyCatch($transaction, (string) $entry->getSourceName(), (string) $entry->getSourceIban());
$transaction = $this->positiveTransactionSafetyCatch($transaction, (string)$entry->getSourceName(), (string)$entry->getSourceIban());
Log::debug(sprintf('destination_id = %d, source_name = "%s", source_iban = "%s", source_id = "%s"', $transaction['destination_id'] ?? '', $transaction['source_name'] ?? '', $transaction['source_iban'] ?? '', $transaction['source_id'] ?? ''));
@ -341,7 +344,7 @@ class GenerateTransactions
break;
}
// temp measure to make sure it's a string:
$iban = (string) $iban;
$iban = (string)$iban;
Log::debug('Done collecting account numbers and names.');
if ('' !== $number) {
@ -355,7 +358,7 @@ class GenerateTransactions
}
// The data importer determines the account type based on the IBAN.
$accountType = (string) ($this->targetTypes[$iban] ?? 'unknown');
$accountType = (string)($this->targetTypes[$iban] ?? 'unknown');
// If the IBAN is a known target account, but it's not a liability OR revenue OR expense, the data importer knows for sure this is a transfer.
// it will save the ID and nothing else.
@ -373,7 +376,7 @@ class GenerateTransactions
$accountName = $this->getRevenueOrExpenseName($iban, $accountType);
if ($bonusTag !== $accountName) {
Log::debug(sprintf('Add account name "%s" as extra tag because the recognized account is called "%s".', $bonusTag, $accountName));
$transaction['bonus_tags'][] = $bonusTag;
$transaction['notes'] = sprintf("%s\n\nOriginal account name: %s", $transaction['notes'], $bonusTag);
}
}
@ -389,7 +392,7 @@ class GenerateTransactions
// If the account number is a known target account, but it's not a liability, the data importer knows for sure this is a transfer.
// it will save the ID and nothing else.
$accountType = (string) ($this->targetTypes[$number] ?? 'unknown');
$accountType = (string)($this->targetTypes[$number] ?? 'unknown');
if ($this->isAssetAccount($accountType, $number) && sprintf(self::NUMBER_FORMAT, '') !== $number) {
Log::debug(sprintf('Recognized "%s" (number) as a Firefly III asset account so this is a transfer.', $number));
$transaction[$idKey] = $this->targetAccounts[$number];
@ -415,7 +418,7 @@ class GenerateTransactions
private function getMappedAccountId(string $name): ?int
{
if (isset($this->configuration->getMapping()['accounts'][$name])) {
return (int) $this->configuration->getMapping()['accounts'][$name];
return (int)$this->configuration->getMapping()['accounts'][$name];
}
return null;
@ -457,7 +460,7 @@ class GenerateTransactions
$request->setTimeOut(config('importer.connection.timeout'));
$request->setId($accountId);
// @var GetAccountResponse $result
/** @var GetAccountResponse $result */
try {
$result = $request->get();
} catch (ApiHttpException $e) {
@ -502,7 +505,7 @@ class GenerateTransactions
private function appendNegativeAmountInfo(string $accountId, array $transaction, Transaction $entry): array
{
$transaction['amount'] = bcmul($entry->transactionAmount, '-1');
$transaction['source_id'] = (int) $this->accounts[$accountId]; // TODO entry may not exist, then what?
$transaction['source_id'] = (int)$this->accounts[$accountId]; // TODO entry may not exist, then what?
// append source iban and number (if present)
$transaction = $this->appendAccountFields($transaction, $entry, 'destination');
@ -535,7 +538,7 @@ class GenerateTransactions
}
}
$transaction = $this->negativeTransactionSafetyCatch($transaction, (string) $entry->getDestinationName(), (string) $entry->getDestinationIban());
$transaction = $this->negativeTransactionSafetyCatch($transaction, (string)$entry->getDestinationName(), (string)$entry->getDestinationIban());
Log::debug(sprintf('source_id = %d, destination_id = "%s", destination_name = "%s", destination_iban = "%s"', $transaction['source_id'], $transaction['destination_id'] ?? '', $transaction['destination_name'] ?? '', $transaction['destination_iban'] ?? ''));

11
app/Services/Nordigen/Conversion/Routine/TransactionProcessor.php

@ -74,9 +74,10 @@ class TransactionProcessor
$return = [];
Log::debug(sprintf('Found %d accounts to download from.', count($accounts)));
$total = count($accounts);
foreach ($accounts as $key => $account) {
$account = (string) $account;
Log::debug(sprintf('Going to download transactions for account #%d "%s"', $key, $account));
Log::debug(sprintf('[%d/%d] Going to download transactions for account #%d "%s"', $key + 1, $total, $key + 1, $account));
$object = new Account();
$object->setIdentifier($account);
$fullInfo = null;
@ -113,13 +114,13 @@ class TransactionProcessor
$request = new GetTransactionsRequest($url, $accessToken, $account);
$request->setTimeOut(config('importer.connection.timeout'));
// @var GetTransactionsResponse $transactions
/** @var GetTransactionsResponse $transactions */
try {
$transactions = $request->get();
Log::debug(sprintf('GetTransactionsResponse: count %d transaction(s)', count($transactions)));
} catch (ImporterHttpException|RateLimitException $e) {
Log::debug(sprintf('Ran into %s instead of GetTransactionsResponse', $e::class));
$this->addError(0, $e->getMessage());
$this->addWarning(0, $e->getMessage());
$return[$account] = [];
// save the rate limits:
@ -148,9 +149,9 @@ class TransactionProcessor
];
$return[$account] = $this->filterTransactions($transactions);
Log::debug(sprintf('Done downloading transactions for account %s "%s"', $key, $account));
Log::debug(sprintf('[%d/%d] Done downloading transactions for account #%d "%s"', $key + 1, $total, $key + 1, $account));
}
Log::debug('Done with download');
Log::debug('Done with download of transactions.');
return $return;
}

20
app/Services/Nordigen/Conversion/RoutineManager.php

@ -83,11 +83,17 @@ class RoutineManager implements RoutineManagerInterface
return $this->transactionProcessor->getAccounts();
}
/**
* @throws ImporterErrorException
*/
public function setConfiguration(Configuration $configuration): void
{
// save config
$this->configuration = $configuration;
// Step 0: configuration validation.
$this->validateAccounts();
// share config
$this->transactionProcessor->setConfiguration($configuration);
$this->transactionGenerator->setConfiguration($configuration);
@ -357,4 +363,18 @@ class RoutineManager implements RoutineManagerInterface
throw new ImporterErrorException($e->getMessage(), 0, $e);
}
}
/**
* @throws ImporterErrorException
*/
private function validateAccounts(): void
{
Log::debug('Validating accounts in configuration.');
$accounts = $this->configuration->getAccounts();
foreach ($accounts as $key => $accountId) {
if (0 === (int)$accountId) {
throw new ImporterErrorException(sprintf('Cannot import GoCardless account "%s" into Firefly III account #%d. Recreate your configuration file.', $key, $accountId));
}
}
}
}

4
app/Services/Nordigen/Services/AccountInformationCollector.php

@ -207,6 +207,10 @@ class AccountInformationCollector
Log::debug('Set new IBAN from basic details.');
$account->setIban($array['iban']);
}
if (array_key_exists('owner_name', $array) && '' !== $array['owner_name'] && '' === $account->getOwnerName()) {
Log::debug('Set new owner name from basic details.');
$account->setOwnerName($array['owner_name']);
}
return $account;
}

5
app/Support/Internal/CollectsAccounts.php

@ -58,7 +58,6 @@ trait CollectsAccounts
Constants::LIABILITIES => [],
];
$url = null;
$token = null;
try {
$url = SecretManager::getBaseUrl();
@ -140,9 +139,11 @@ trait CollectsAccounts
Log::debug(sprintf('Now in %s', __METHOD__));
$requisitions = $configuration->getNordigenRequisitions();
$identifier = array_shift($requisitions);
$inCache = Cache::has($identifier) && config('importer.use_cache');
$inCache = false;
// if cached, return it.
if (Cache::has($identifier) && config('importer.use_cache')) {
if ($inCache) {
$result = Cache::get($identifier);
$return = [];
foreach ($result as $arr) {

97
app/Support/Internal/MergesAccountLists.php

@ -46,56 +46,79 @@ trait MergesAccountLists
/** @var ImportServiceAccount $account */
foreach ($generic as $account) {
Log::debug(sprintf('Working on generic account "%s": "%s" ("%s", "%s")', $account->name, $account->id, $account->iban, $account->bban));
Log::debug(sprintf('Working on generic account name: "%s": id:"%s" (iban:"%s", number:"%s")', $account->name, $account->id, $account->iban, $account->bban));
$iban = $account->iban;
$number = $account->bban;
$currency = $account->currencyCode;
$entry = [
'import_account' => $account,
'import_account' => $account,
'firefly_iii_accounts' => [
Constants::ASSET_ACCOUNTS => [],
Constants::LIABILITIES => [],
],
];
$filteredByNumber = $this->filterByAccountNumber($fireflyIII, $iban, $number);
$filteredByCurrency = $this->filterByCurrency($fireflyIII, $currency);
if (1 === count($filteredByNumber)) {
Log::debug(sprintf('Generic account ("%s", "%s") has a single FF3 counter part (#%d, "%s")', $iban, $number, $filteredByNumber[0]->id, $filteredByNumber[0]->name));
$entry['firefly_iii_accounts'] = array_unique(array_merge($filteredByNumber, $filteredByCurrency), SORT_REGULAR);
$return[] = $entry;
continue;
Log::debug('Filtered by number', $filteredByNumber);
Log::debug('Filtered by currency', $filteredByCurrency);
$count = 0;
foreach ([Constants::ASSET_ACCOUNTS, Constants::LIABILITIES] as $key) {
if (1 === count($filteredByNumber[$key])) {
Log::debug(sprintf('Generic account ("%s", "%s") has a single FF3 %s counter part (#%d, "%s")', $iban, $number, $key, $filteredByNumber[$key][0]->id, $filteredByNumber[$key][0]->name));
$entry['firefly_iii_accounts'][$key] = array_unique(array_merge(
$filteredByNumber[$key],
$filteredByCurrency[$key]
), SORT_REGULAR);
$return[] = $entry;
++$count;
continue 2;
}
}
Log::debug(sprintf('Found %d FF3 accounts with the same IBAN or number ("%s")', count($filteredByNumber), $iban));
if (count($filteredByCurrency) > 0) {
Log::debug(sprintf('Generic account ("%s") has %d Firefly III counter part(s) with the same currency %s.', $account->name, count($filteredByCurrency), $currency));
$entry['firefly_iii_accounts'] = $filteredByCurrency;
$return[] = $entry;
Log::debug(sprintf('Found %d FF3 accounts with the same IBAN or number ("%s")', $count, $iban));
unset($count);
foreach ([Constants::ASSET_ACCOUNTS, Constants::LIABILITIES] as $key) {
if (count($filteredByCurrency[$key]) > 0) {
Log::debug(sprintf('Generic account ("%s") has %d Firefly III %s counter part(s) with the same currency %s.', $account->name, $key, count($filteredByCurrency), $currency));
$entry['firefly_iii_accounts'][$key] = $filteredByCurrency[$key];
$return[] = $entry;
continue;
continue 2;
}
}
Log::debug('No special filtering on the Firefly III account list.');
$entry['firefly_iii_accounts'] = array_merge($fireflyIII[Constants::ASSET_ACCOUNTS], $fireflyIII[Constants::LIABILITIES]);
// remove array_merge because SimpleFIN does not do this so it broke all the other importer routines.
$entry['firefly_iii_accounts'] = $fireflyIII;
$return[] = $entry;
}
return $return;
}
protected function filterByAccountNumber(array $firefly, string $iban, string $number): array
protected function filterByAccountNumber(array $fireflyIII, string $iban, string $number): array
{
Log::debug(sprintf('Now filtering Firefly III accounts by IBAN "%s" or number "%s".', $iban, $number));
// FIXME this check should also check the number of the account.
if ('' === $iban) {
return [];
return [
Constants::ASSET_ACCOUNTS => [],
Constants::LIABILITIES => [],
];
}
$result = [];
// TODO check if this the correct merge type.
$all = array_merge($firefly[Constants::ASSET_ACCOUNTS] ?? [], $firefly[Constants::LIABILITIES] ?? []);
/** @var Account $account */
foreach ($all as $account) {
if ($iban === $account->iban || $number === $account->number || $iban === $account->number || $number === $account->iban) {
$result[] = $account;
$result = [
Constants::ASSET_ACCOUNTS => [],
Constants::LIABILITIES => [],
];
foreach ($fireflyIII as $key => $accounts) {
foreach ($accounts as $account) {
if ($iban === $account->iban || $number === $account->number || $iban === $account->number || $number === $account->iban) {
$result[$key][] = $account;
}
}
}
@ -105,15 +128,21 @@ trait MergesAccountLists
protected function filterByCurrency(array $fireflyIII, string $currency): array
{
if ('' === $currency) {
return [];
return [
Constants::ASSET_ACCOUNTS => [],
Constants::LIABILITIES => [],
];
}
$result = [];
$all = array_merge($fireflyIII[Constants::ASSET_ACCOUNTS] ?? [], $fireflyIII[Constants::LIABILITIES] ?? []);
/** @var Account $account */
foreach ($all as $account) {
if ($currency === $account->currencyCode) {
$result[] = $account;
$result = [
Constants::ASSET_ACCOUNTS => [],
Constants::LIABILITIES => [],
];
foreach ($fireflyIII as $key => $accounts) {
foreach ($accounts as $account) {
if ($currency === $account->currencyCode) {
$result[$key][] = $account;
}
}
}

12
changelog.md

@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## v1.7.1 - 2025-07-02
> ⚠️ Some changes in this release may unexpectedly lead to duplicate transactions. This is caused by changes in the data handling routines. This is unfortunate, but a result of new insights, changed APIs and other minor fixes. My apologies for any inconvenience. I try to avoid these kinds of changes, but it can't always be helped.
### Changed
- For GoCardless imports: opposing account names and other reference data is no longer added as tags, but as text in the notes. This may lead to duplicates.
- GoCardless: better handling of rate limits.
### Fixed
- [Issue 10508](https://github.com/firefly-iii/firefly-iii/issues/10508) (Firefly Version 1.7.0 uses SimpleFIN for GoCardless and crashes when importing) reported by @L3tum
- Improved parsing of dates.
## v1.7.0 - 2025-06-26
### Added

154
composer.lock

@ -516,12 +516,12 @@
"source": {
"type": "git",
"url": "https://github.com/JC5/api-support-classes.git",
"reference": "83127041fcf522c9548f8016569fe5d4403f2d83"
"reference": "c70158bc66bb3da01c9b7547539f1331ea2282ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JC5/api-support-classes/zipball/83127041fcf522c9548f8016569fe5d4403f2d83",
"reference": "83127041fcf522c9548f8016569fe5d4403f2d83",
"url": "https://api.github.com/repos/JC5/api-support-classes/zipball/c70158bc66bb3da01c9b7547539f1331ea2282ce",
"reference": "c70158bc66bb3da01c9b7547539f1331ea2282ce",
"shasum": ""
},
"require": {
@ -615,7 +615,7 @@
"type": "patreon"
}
],
"time": "2025-05-02T03:42:46+00:00"
"time": "2025-06-29T14:43:41+00:00"
},
{
"name": "fruitcake/php-cors",
@ -3611,16 +3611,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44"
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44",
"url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101",
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101",
"shasum": ""
},
"require": {
@ -3685,7 +3685,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.0"
"source": "https://github.com/symfony/console/tree/v7.3.1"
},
"funding": [
{
@ -3701,7 +3701,7 @@
"type": "tidelift"
}
],
"time": "2025-05-24T10:34:04+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/css-selector",
@ -3837,16 +3837,16 @@
},
{
"name": "symfony/error-handler",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "cf68d225bc43629de4ff54778029aee6dc191b83"
"reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83",
"reference": "cf68d225bc43629de4ff54778029aee6dc191b83",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235",
"reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235",
"shasum": ""
},
"require": {
@ -3894,7 +3894,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v7.3.0"
"source": "https://github.com/symfony/error-handler/tree/v7.3.1"
},
"funding": [
{
@ -3910,7 +3910,7 @@
"type": "tidelift"
}
],
"time": "2025-05-29T07:19:49+00:00"
"time": "2025-06-13T07:48:40+00:00"
},
{
"name": "symfony/event-dispatcher",
@ -4134,16 +4134,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9"
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9",
"url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64",
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64",
"shasum": ""
},
"require": {
@ -4155,6 +4155,7 @@
},
"conflict": {
"amphp/amp": "<2.5",
"amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
@ -4167,7 +4168,6 @@
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
@ -4209,7 +4209,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.0"
"source": "https://github.com/symfony/http-client/tree/v7.3.1"
},
"funding": [
{
@ -4225,7 +4225,7 @@
"type": "tidelift"
}
],
"time": "2025-05-02T08:23:16+00:00"
"time": "2025-06-28T07:58:39+00:00"
},
{
"name": "symfony/http-client-contracts",
@ -4307,16 +4307,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "4236baf01609667d53b20371486228231eb135fd"
"reference": "23dd60256610c86a3414575b70c596e5deff6ed9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd",
"reference": "4236baf01609667d53b20371486228231eb135fd",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9",
"reference": "23dd60256610c86a3414575b70c596e5deff6ed9",
"shasum": ""
},
"require": {
@ -4366,7 +4366,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.3.0"
"source": "https://github.com/symfony/http-foundation/tree/v7.3.1"
},
"funding": [
{
@ -4382,20 +4382,20 @@
"type": "tidelift"
}
],
"time": "2025-05-12T14:48:23+00:00"
"time": "2025-06-23T15:07:14+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f"
"reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f",
"reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831",
"reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831",
"shasum": ""
},
"require": {
@ -4480,7 +4480,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.3.0"
"source": "https://github.com/symfony/http-kernel/tree/v7.3.1"
},
"funding": [
{
@ -4496,20 +4496,20 @@
"type": "tidelift"
}
],
"time": "2025-05-29T07:47:32+00:00"
"time": "2025-06-28T08:24:55+00:00"
},
{
"name": "symfony/mailer",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c"
"reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/0f375bbbde96ae8c78e4aa3e63aabd486e33364c",
"reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c",
"url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368",
"reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368",
"shasum": ""
},
"require": {
@ -4560,7 +4560,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v7.3.0"
"source": "https://github.com/symfony/mailer/tree/v7.3.1"
},
"funding": [
{
@ -4576,20 +4576,20 @@
"type": "tidelift"
}
],
"time": "2025-04-04T09:51:09+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/mailgun-mailer",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailgun-mailer.git",
"reference": "3c1dfd9ff0a487a4116baec42d11ae21a061e3f1"
"reference": "8c18f2bff4e70ed5669ab8228302edd2fecd689b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/3c1dfd9ff0a487a4116baec42d11ae21a061e3f1",
"reference": "3c1dfd9ff0a487a4116baec42d11ae21a061e3f1",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/8c18f2bff4e70ed5669ab8228302edd2fecd689b",
"reference": "8c18f2bff4e70ed5669ab8228302edd2fecd689b",
"shasum": ""
},
"require": {
@ -4629,7 +4629,7 @@
"description": "Symfony Mailgun Mailer Bridge",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.3.0"
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.3.1"
},
"funding": [
{
@ -4645,7 +4645,7 @@
"type": "tidelift"
}
],
"time": "2024-09-28T08:24:38+00:00"
"time": "2025-06-20T16:15:52+00:00"
},
{
"name": "symfony/mime",
@ -5749,16 +5749,16 @@
},
{
"name": "symfony/translation",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "4aba29076a29a3aa667e09b791e5f868973a8667"
"reference": "241d5ac4910d256660238a7ecf250deba4c73063"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667",
"reference": "4aba29076a29a3aa667e09b791e5f868973a8667",
"url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063",
"reference": "241d5ac4910d256660238a7ecf250deba4c73063",
"shasum": ""
},
"require": {
@ -5825,7 +5825,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.3.0"
"source": "https://github.com/symfony/translation/tree/v7.3.1"
},
"funding": [
{
@ -5841,7 +5841,7 @@
"type": "tidelift"
}
],
"time": "2025-05-29T07:19:49+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/translation-contracts",
@ -5923,16 +5923,16 @@
},
{
"name": "symfony/uid",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
"reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3"
"reference": "a69f69f3159b852651a6bf45a9fdd149520525bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/uid/zipball/7beeb2b885cd584cd01e126c5777206ae4c3c6a3",
"reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3",
"url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb",
"reference": "a69f69f3159b852651a6bf45a9fdd149520525bb",
"shasum": ""
},
"require": {
@ -5977,7 +5977,7 @@
"uuid"
],
"support": {
"source": "https://github.com/symfony/uid/tree/v7.3.0"
"source": "https://github.com/symfony/uid/tree/v7.3.1"
},
"funding": [
{
@ -5993,20 +5993,20 @@
"type": "tidelift"
}
],
"time": "2025-05-24T14:28:13+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e"
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"shasum": ""
},
"require": {
@ -6061,7 +6061,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.0"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.1"
},
"funding": [
{
@ -6077,7 +6077,7 @@
"type": "tidelift"
}
],
"time": "2025-04-27T18:39:23+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@ -8247,16 +8247,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.2.3",
"version": "12.2.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "60a8ea2d8b2f070000051b56778009e11576e7d1"
"reference": "b71849b29f7a8d7574e4401873cb8b539896613f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60a8ea2d8b2f070000051b56778009e11576e7d1",
"reference": "60a8ea2d8b2f070000051b56778009e11576e7d1",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b71849b29f7a8d7574e4401873cb8b539896613f",
"reference": "b71849b29f7a8d7574e4401873cb8b539896613f",
"shasum": ""
},
"require": {
@ -8324,7 +8324,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.3"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.5"
},
"funding": [
{
@ -8348,7 +8348,7 @@
"type": "tidelift"
}
],
"time": "2025-06-20T11:33:06+00:00"
"time": "2025-06-27T04:37:55+00:00"
},
{
"name": "rector/rector",
@ -8416,12 +8416,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "b79895bbbfcb549006a8ee8efae7f77edffe5329"
"reference": "d4ca0cc4c49ba3437778e201d35844715d9b1bd9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b79895bbbfcb549006a8ee8efae7f77edffe5329",
"reference": "b79895bbbfcb549006a8ee8efae7f77edffe5329",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/d4ca0cc4c49ba3437778e201d35844715d9b1bd9",
"reference": "d4ca0cc4c49ba3437778e201d35844715d9b1bd9",
"shasum": ""
},
"conflict": {
@ -8491,7 +8491,7 @@
"bedita/bedita": "<4",
"bednee/cooluri": "<1.0.30",
"bigfork/silverstripe-form-capture": ">=3,<3.1.1",
"billz/raspap-webgui": "<=3.1.4",
"billz/raspap-webgui": "<3.3.6",
"bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
"blueimp/jquery-file-upload": "==6.4.4",
"bmarshall511/wordpress_zero_spam": "<5.2.13",
@ -9100,7 +9100,7 @@
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
"ssddanbrown/bookstack": "<24.05.1",
"starcitizentools/citizen-skin": ">=2.4.2,<3.3.1",
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2",
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
"statamic/cms": "<=5.16",
"stormpath/sdk": "<9.9.99",
"studio-42/elfinder": "<=2.1.64",
@ -9361,7 +9361,7 @@
"type": "tidelift"
}
],
"time": "2025-06-24T22:05:35+00:00"
"time": "2025-06-27T21:05:09+00:00"
},
{
"name": "sebastian/cli-parser",

4
config/importer.php

@ -24,7 +24,7 @@
declare(strict_types=1);
return [
'version' => '1.7.0',
'version' => '1.7.1',
'flows' => ['nordigen', 'spectre', 'file', 'simplefin'],
'enabled_flows' => [
'nordigen' => true,
@ -68,7 +68,7 @@ return [
'ignore_not_found_transactions' => env('IGNORE_NOT_FOUND_TRANSACTIONS', false),
'namespace' => 'c40dcba2-411d-11ec-973a-0242ac130003',
'use_cache' => env('USE_CACHE', false),
'minimum_version' => '6.2.16',
'minimum_version' => '6.2.20',
'cache_api_calls' => false,
'ignored_files' => ['.gitignore'],
'tracker_site_id' => env('TRACKER_SITE_ID', ''),

2
readme.md

@ -34,7 +34,7 @@
The data importer does not connect to your bank directly. Instead, it uses [GoCardless](https://gocardless.com/) and [SaltEdge](https://www.saltedge.com/products/spectre/countries) to connect to over 6000 banks worldwide. These services are free for Firefly III users, but require registration. Keep in mind these services have their own privacy and data usage policies.
The data importer can import CSV files you've downloaded from your bank.
The data importer can also connect to your bank using `SimpleFIN`, and it can import CSV files you've downloaded from your bank.
You can run the data importer once, for a bulk import. You can also run it regularly to keep up with new transactions.

2
resources/views/v2/components/firefly-iii-account-generic.blade.php

@ -6,6 +6,7 @@
id="account-select-{{ $account['import_account']->id }}">
<!-- Create New Account option -->
@if('simplefin' === $flow)
<option value="create_new"
@php
$configuredAccount = $configuration->getAccounts()[$account['import_account']->id] ?? null;
@ -13,6 +14,7 @@
$isCreateNewSelected = (!$configuredAccount || $configuredAccount === 'create_new') && !$mappedTo;
@endphp
@if($isCreateNewSelected) selected @endif> Create New Account</option>
@endif
<!-- loop all Firefly III account groups (assets, liabilities) -->
@foreach($account['firefly_iii_accounts'] as $accountGroupKey => $accountGroup)

3
resources/views/v2/components/importer-account-title.blade.php

@ -43,7 +43,7 @@
@if('' !== $account['import_account']->iban) title="IBAN: {{ $account['import_account']->iban }}" @endif
>
<div class="d-flex align-items-center mb-1">
<span class="fw-bold fs-6">{{ $account['import_account']->name ?? 'Unnamed SimpleFIN Account' }}</span>
<span class="fw-bold fs-6">{{ $account['import_account']->name ?? 'Unnamed account' }}</span>
</div>
@if(isset($account['import_account']->org) && is_array($account['import_account']->org) && !empty($account['import_account']->org['name']))
<div class="text-muted small">
@ -87,6 +87,7 @@
@endif
</div>
{{-- Display 'mapped_to' if available --}}
{{-- Display 'extra' fields if any --}}
@php $extraData = (array)($account['import_account']->extra ?? []); @endphp
@if(count($extraData) > 0)

12
resources/views/v2/components/importer-account.blade.php

@ -13,18 +13,22 @@
<!-- Firefly III Account Content - Visibility Controlled -->
<div id="firefly-account-content-{{ $account['import_account']->id }}">
<!-- TODO this is one of those things to merge into one generic type -->
@if( $flow !== 'simplefin' && (!isset($account['firefly_iii_accounts']['assets']) || count($account['firefly_iii_accounts']['assets']) === 0) && (!isset($account['firefly_iii_accounts']['liabilities']) || count($account['firefly_iii_accounts']['liabilities']) === 0) )
<span class="text-danger">There are no Firefly III accounts to import into</span>
@if(
// flow is not simplefin.
$flow !== 'simplefin' &&
((!isset($account['firefly_iii_accounts']['assets']) || count($account['firefly_iii_accounts']['assets']) === 0) && (!isset($account['firefly_iii_accounts']['liabilities']) || count($account['firefly_iii_accounts']['liabilities']) === 0) )
)
<span class="text-danger">X There are no Firefly III accounts to import into</span>
@endif
@if( $flow === 'simplefin' || (isset($account['firefly_iii_accounts']['assets']) && count($account['firefly_iii_accounts']['assets']) > 0) || (isset($account['firefly_iii_accounts']['liabilities']) && count($account['firefly_iii_accounts']['liabilities']) > 0) )
<x-firefly-iii-account-generic :account="$account" :configuration="$configuration"/>
<x-firefly-iii-account-generic :flow="$flow" :account="$account" :configuration="$configuration"/>
<x-create-account-widget :account="$account" :configuration="$configuration" :currencies="$currencies ?? []"/>
@endif
</div>
<!-- Not Imported Text - Hidden by Default -->
<div id="not-imported-text-{{ $account['import_account']->id }}" style="display: none;" class="text-muted py-2">
<small><i class="fas fa-info-circle fa-sm me-1"></i>Not Imported</small>
<small><i class="fas fa-info-circle fa-sm me-1"></i>Not imported</small>
</div>
</td>
</tr>
Loading…
Cancel
Save