From 2ab9d2e6ee2af97f5728c4ab1c8f5311f9ab3445 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 10 Jun 2019 20:14:00 +0200 Subject: [PATCH] Improve test coverage. --- .deploy/docker/entrypoint.sh | 2 +- .../Controllers/Chart/AccountController.php | 4 - .../Controllers/Chart/CategoryController.php | 3 - app/Api/V1/Controllers/CurrencyController.php | 8 +- .../V1/Controllers/PreferenceController.php | 2 +- .../V1/Controllers/RuleGroupController.php | 3 +- app/Api/V1/Controllers/SummaryController.php | 5 +- app/Api/V1/Controllers/TagController.php | 2 - .../Controllers/TransactionLinkController.php | 2 +- app/Api/V1/Requests/AccountStoreRequest.php | 2 +- app/Api/V1/Requests/BillRequest.php | 2 +- app/Api/V1/Requests/RuleGroupTestRequest.php | 1 + app/Api/V1/Requests/RuleTestRequest.php | 1 + app/Api/V1/Requests/RuleTriggerRequest.php | 1 + .../Commands/Correction/CorrectDatabase.php | 3 +- .../Correction/CreateAccessTokens.php | 9 +- .../Commands/Correction/CreateLinkTypes.php | 5 +- .../Commands/Correction/DeleteEmptyGroups.php | 2 +- .../Correction/DeleteEmptyJournals.php | 15 +- .../Correction/DeleteOrphanedTransactions.php | 11 +- .../Commands/Correction/DeleteZeroAmount.php | 9 +- .../Commands/Correction/EnableCurrencies.php | 6 +- .../Commands/Correction/FixAccountTypes.php | 174 +++---- .../Commands/Correction/FixPiggies.php | 57 ++- .../Commands/Correction/FixUnevenAmount.php | 9 +- .../Commands/Correction/RenameMetaFields.php | 19 +- .../Commands/Correction/TransferBudgets.php | 3 + app/Console/Commands/DecryptDatabase.php | 8 +- .../Commands/Import/CreateCSVImport.php | 307 +++++++------ .../Commands/Integrity/ReportEmptyObjects.php | 13 +- .../Commands/Integrity/ReportIntegrity.php | 1 + app/Console/Commands/Tools/ApplyRules.php | 71 +-- .../Commands/Upgrade/AccountCurrencies.php | 72 ++- .../Commands/Upgrade/BackToJournals.php | 41 +- .../Commands/Upgrade/BudgetLimitCurrency.php | 3 + .../Commands/Upgrade/CCLiabilities.php | 5 + .../Commands/Upgrade/JournalCurrencies.php | 125 +++-- .../Controllers/System/InstallController.php | 2 +- app/Models/Account.php | 18 - app/Models/PiggyBankEvent.php | 4 - app/Providers/ImportServiceProvider.php | 61 +++ .../Account/AccountRepository.php | 36 +- .../Account/AccountRepositoryInterface.php | 20 +- .../Journal/JournalRepository.php | 15 + .../Journal/JournalRepositoryInterface.php | 32 +- app/Validation/FireflyValidator.php | 17 +- config/app.php | 3 + database/seeds/LinkTypeSeeder.php | 9 +- phpunit.coverage.specific.xml | 22 +- phpunit.coverage.xml | 22 +- phpunit.xml | 22 +- tests/TestCase.php | 3 + .../Correction/CreateAccessTokensTest.php | 96 ++++ .../Correction/CreateLinkTypesTest.php | 78 ++++ .../Correction/DeleteEmptyGroupsTest.php | 70 +++ .../Correction/DeleteEmptyJournalsTest.php | 116 +++++ .../DeleteOrphanedTransactionsTest.php | 144 ++++++ .../Correction/DeleteZeroAmountTest.php | 91 ++++ .../Correction/EnableCurrenciesTest.php | 82 ++++ .../Correction/FixAccountTypesTest.php | 371 +++++++++++++++ .../Commands/Correction/FixPiggiesTest.php | 130 ++++++ .../Correction/FixUnevenAmountTest.php | 101 ++++ .../Commands/Correction/RemoveBillsTest.php | 75 +++ .../Correction/RenameMetaFieldsTest.php | 76 +++ .../Correction/TransferBudgetsTest.php | 71 +++ .../Console/Commands/DecryptDatabaseTest.php | 140 ++++++ .../Commands/Import/CreateCSVImportTest.php | 433 ++++++++++++++++++ .../Integrity/ReportEmptyObjectsTest.php | 148 ++++++ .../Commands/Integrity/ReportSumTest.php | 85 ++++ .../Console/Commands/Tools/ApplyRulesTest.php | 404 ++++++++++++++++ .../Upgrade/AccountCurrenciesTest.php | 341 ++++++++++++++ .../Commands/Upgrade/BackToJournalsTest.php | 244 ++++++++++ .../Upgrade/BudgetLimitCurrencyTest.php | 85 ++++ .../Commands/Upgrade/CCLiabilitiesTest.php | 129 ++++++ .../Upgrade/JournalCurrenciesTest.php | 354 ++++++++++++++ 75 files changed, 4672 insertions(+), 484 deletions(-) create mode 100644 app/Providers/ImportServiceProvider.php create mode 100644 tests/Unit/Console/Commands/Correction/CreateAccessTokensTest.php create mode 100644 tests/Unit/Console/Commands/Correction/CreateLinkTypesTest.php create mode 100644 tests/Unit/Console/Commands/Correction/DeleteEmptyGroupsTest.php create mode 100644 tests/Unit/Console/Commands/Correction/DeleteEmptyJournalsTest.php create mode 100644 tests/Unit/Console/Commands/Correction/DeleteOrphanedTransactionsTest.php create mode 100644 tests/Unit/Console/Commands/Correction/DeleteZeroAmountTest.php create mode 100644 tests/Unit/Console/Commands/Correction/EnableCurrenciesTest.php create mode 100644 tests/Unit/Console/Commands/Correction/FixAccountTypesTest.php create mode 100644 tests/Unit/Console/Commands/Correction/FixPiggiesTest.php create mode 100644 tests/Unit/Console/Commands/Correction/FixUnevenAmountTest.php create mode 100644 tests/Unit/Console/Commands/Correction/RemoveBillsTest.php create mode 100644 tests/Unit/Console/Commands/Correction/RenameMetaFieldsTest.php create mode 100644 tests/Unit/Console/Commands/Correction/TransferBudgetsTest.php create mode 100644 tests/Unit/Console/Commands/DecryptDatabaseTest.php create mode 100644 tests/Unit/Console/Commands/Import/CreateCSVImportTest.php create mode 100644 tests/Unit/Console/Commands/Integrity/ReportEmptyObjectsTest.php create mode 100644 tests/Unit/Console/Commands/Integrity/ReportSumTest.php create mode 100644 tests/Unit/Console/Commands/Tools/ApplyRulesTest.php create mode 100644 tests/Unit/Console/Commands/Upgrade/AccountCurrenciesTest.php create mode 100644 tests/Unit/Console/Commands/Upgrade/BackToJournalsTest.php create mode 100644 tests/Unit/Console/Commands/Upgrade/BudgetLimitCurrencyTest.php create mode 100644 tests/Unit/Console/Commands/Upgrade/CCLiabilitiesTest.php create mode 100644 tests/Unit/Console/Commands/Upgrade/JournalCurrenciesTest.php diff --git a/.deploy/docker/entrypoint.sh b/.deploy/docker/entrypoint.sh index 656eaf6310..807f580500 100755 --- a/.deploy/docker/entrypoint.sh +++ b/.deploy/docker/entrypoint.sh @@ -59,7 +59,7 @@ fi #env $(grep -v "^\#" .env | xargs) php artisan cache:clear php artisan migrate --seed -php artisan firefly:decrypt-all +php artisan firefly-iii:decrypt-all # upgrade database commands: php artisan firefly-iii:transaction-identifiers diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index bd0fa96832..964e21c3ca 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -27,7 +27,6 @@ namespace FireflyIII\Api\V1\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\DateRequest; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; @@ -74,7 +73,6 @@ class AccountController extends Controller * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException */ public function expenseOverview(DateRequest $request): JsonResponse { @@ -161,7 +159,6 @@ class AccountController extends Controller * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException */ public function overview(DateRequest $request): JsonResponse { @@ -224,7 +221,6 @@ class AccountController extends Controller * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException */ public function revenueOverview(DateRequest $request): JsonResponse { diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index 2a1366306c..fd1d4d08a1 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -27,11 +27,9 @@ namespace FireflyIII\Api\V1\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\DateRequest; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\User; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Collection; /** @@ -66,7 +64,6 @@ class CategoryController extends Controller * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException */ public function overview(DateRequest $request): JsonResponse { diff --git a/app/Api/V1/Controllers/CurrencyController.php b/app/Api/V1/Controllers/CurrencyController.php index 845d9efe8a..04e96231cb 100644 --- a/app/Api/V1/Controllers/CurrencyController.php +++ b/app/Api/V1/Controllers/CurrencyController.php @@ -128,7 +128,7 @@ class CurrencyController extends Controller // filter list on currency preference: $collection = $unfiltered->filter( - function (Account $account) use ($currency, $accountRepository) { + static function (Account $account) use ($currency, $accountRepository) { $currencyId = (int)$accountRepository->getMetaValue($account, 'currency_id'); return $currencyId === $currency->id; @@ -229,7 +229,7 @@ class CurrencyController extends Controller // filter and paginate list: $collection = $unfiltered->filter( - function (Bill $bill) use ($currency) { + static function (Bill $bill) use ($currency) { return $bill->transaction_currency_id === $currency->id; } ); @@ -505,7 +505,7 @@ class CurrencyController extends Controller // filter selection $collection = $unfiltered->filter( - function (Recurrence $recurrence) use ($currency) { + static function (Recurrence $recurrence) use ($currency) { /** @var RecurrenceTransaction $transaction */ foreach ($recurrence->recurrenceTransactions as $transaction) { if ($transaction->transaction_currency_id === $currency->id || $transaction->foreign_currency_id === $currency->id) { @@ -560,7 +560,7 @@ class CurrencyController extends Controller $unfiltered = $repository->getAll(); $collection = $unfiltered->filter( - function (Rule $rule) use ($currency) { + static function (Rule $rule) use ($currency) { /** @var RuleTrigger $trigger */ foreach ($rule->ruleTriggers as $trigger) { if ('currency_is' === $trigger->trigger_type && $currency->name === $trigger->trigger_value) { diff --git a/app/Api/V1/Controllers/PreferenceController.php b/app/Api/V1/Controllers/PreferenceController.php index ab01633602..7811875b1a 100644 --- a/app/Api/V1/Controllers/PreferenceController.php +++ b/app/Api/V1/Controllers/PreferenceController.php @@ -52,7 +52,7 @@ class PreferenceController extends Controller { parent::__construct(); $this->middleware( - function ($request, $next) { + static function ($request, $next) { /** @var User $user */ $user = auth()->user(); $repository = app(AccountRepositoryInterface::class); diff --git a/app/Api/V1/Controllers/RuleGroupController.php b/app/Api/V1/Controllers/RuleGroupController.php index 797d17b84b..2f5e22000a 100644 --- a/app/Api/V1/Controllers/RuleGroupController.php +++ b/app/Api/V1/Controllers/RuleGroupController.php @@ -258,7 +258,8 @@ class RuleGroupController extends Controller $matcher->setTriggeredLimit($parameters['trigger_limit']); $matcher->setAccounts($parameters['accounts']); - $result = $matcher->findTransactionsByRule(); + $result = $matcher->findTransactionsByRule(); + /** @noinspection AdditionOperationOnArraysInspection */ $matchingTransactions = $result + $matchingTransactions; } diff --git a/app/Api/V1/Controllers/SummaryController.php b/app/Api/V1/Controllers/SummaryController.php index fd848cbef5..a3fea2543e 100644 --- a/app/Api/V1/Controllers/SummaryController.php +++ b/app/Api/V1/Controllers/SummaryController.php @@ -28,7 +28,6 @@ namespace FireflyIII\Api\V1\Controllers; use Carbon\Carbon; use Exception; use FireflyIII\Api\V1\Requests\DateRequest; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Report\NetWorthInterface; use FireflyIII\Models\Account; @@ -41,7 +40,6 @@ use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\User; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Collection; /** @@ -86,10 +84,9 @@ class SummaryController extends Controller } /** - * @param Request $request + * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException * @throws Exception */ public function basic(DateRequest $request): JsonResponse diff --git a/app/Api/V1/Controllers/TagController.php b/app/Api/V1/Controllers/TagController.php index 2182fa90c9..3246337835 100644 --- a/app/Api/V1/Controllers/TagController.php +++ b/app/Api/V1/Controllers/TagController.php @@ -26,7 +26,6 @@ namespace FireflyIII\Api\V1\Controllers; use Carbon\Carbon; use FireflyIII\Api\V1\Requests\DateRequest; use FireflyIII\Api\V1\Requests\TagRequest; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Tag; use FireflyIII\Repositories\Tag\TagRepositoryInterface; @@ -79,7 +78,6 @@ class TagController extends Controller * @param DateRequest $request * * @return JsonResponse - * @throws FireflyException */ public function cloud(DateRequest $request): JsonResponse { diff --git a/app/Api/V1/Controllers/TransactionLinkController.php b/app/Api/V1/Controllers/TransactionLinkController.php index e0d5cf38ed..65dfe22a42 100644 --- a/app/Api/V1/Controllers/TransactionLinkController.php +++ b/app/Api/V1/Controllers/TransactionLinkController.php @@ -105,7 +105,7 @@ class TransactionLinkController extends Controller $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; // read type from URI - $name = $request->get('name') ?? null; + $name = $request->get('name'); // types to get, page size: $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; diff --git a/app/Api/V1/Requests/AccountStoreRequest.php b/app/Api/V1/Requests/AccountStoreRequest.php index a21b510554..ae7b739142 100644 --- a/app/Api/V1/Requests/AccountStoreRequest.php +++ b/app/Api/V1/Requests/AccountStoreRequest.php @@ -56,7 +56,7 @@ class AccountStoreRequest extends Request $ccPaymentTypes = implode(',', array_keys(config('firefly.ccTypes'))); $rules = [ 'name' => 'required|min:1|uniqueAccountForUser', - 'type' => sprintf('in:%s', $types), + 'type' => 'required|' . sprintf('in:%s', $types), 'iban' => 'iban|nullable', 'bic' => 'bic|nullable', 'account_number' => 'between:1,255|nullable|uniqueAccountNumberForUser', diff --git a/app/Api/V1/Requests/BillRequest.php b/app/Api/V1/Requests/BillRequest.php index 2e9e84274d..a8c86c00f3 100644 --- a/app/Api/V1/Requests/BillRequest.php +++ b/app/Api/V1/Requests/BillRequest.php @@ -119,7 +119,7 @@ class BillRequest extends Request public function withValidator(Validator $validator): void { $validator->after( - function (Validator $validator) { + static function (Validator $validator) { $data = $validator->getData(); $min = (float)($data['amount_min'] ?? 0); $max = (float)($data['amount_max'] ?? 0); diff --git a/app/Api/V1/Requests/RuleGroupTestRequest.php b/app/Api/V1/Requests/RuleGroupTestRequest.php index 22d0492b8b..1b3e9a4c75 100644 --- a/app/Api/V1/Requests/RuleGroupTestRequest.php +++ b/app/Api/V1/Requests/RuleGroupTestRequest.php @@ -123,6 +123,7 @@ class RuleGroupTestRequest extends Request Log::debug(sprintf('Searching for asset account with id "%s"', $accountId)); $account = $accountRepository->findNull((int)$accountId); if ($this->validAccount($account)) { + /** @noinspection NullPointerExceptionInspection */ Log::debug(sprintf('Found account #%d ("%s") and its an asset account', $account->id, $account->name)); $accounts->push($account); } diff --git a/app/Api/V1/Requests/RuleTestRequest.php b/app/Api/V1/Requests/RuleTestRequest.php index 85c4d83b42..44ab7c8114 100644 --- a/app/Api/V1/Requests/RuleTestRequest.php +++ b/app/Api/V1/Requests/RuleTestRequest.php @@ -123,6 +123,7 @@ class RuleTestRequest extends Request Log::debug(sprintf('Searching for asset account with id "%s"', $accountId)); $account = $accountRepository->findNull((int)$accountId); if ($this->validAccount($account)) { + /** @noinspection NullPointerExceptionInspection */ Log::debug(sprintf('Found account #%d ("%s") and its an asset account', $account->id, $account->name)); $accounts->push($account); } diff --git a/app/Api/V1/Requests/RuleTriggerRequest.php b/app/Api/V1/Requests/RuleTriggerRequest.php index bff9a3a7dd..570232751d 100644 --- a/app/Api/V1/Requests/RuleTriggerRequest.php +++ b/app/Api/V1/Requests/RuleTriggerRequest.php @@ -98,6 +98,7 @@ class RuleTriggerRequest extends Request Log::debug(sprintf('Searching for asset account with id "%s"', $accountId)); $account = $accountRepository->findNull((int)$accountId); if ($this->validAccount($account)) { + /** @noinspection NullPointerExceptionInspection */ Log::debug(sprintf('Found account #%d ("%s") and its an asset account', $account->id, $account->name)); $accounts->push($account); } diff --git a/app/Console/Commands/Correction/CorrectDatabase.php b/app/Console/Commands/Correction/CorrectDatabase.php index 212fa9bb6e..6bcb3dc278 100644 --- a/app/Console/Commands/Correction/CorrectDatabase.php +++ b/app/Console/Commands/Correction/CorrectDatabase.php @@ -30,6 +30,7 @@ use Schema; /** * Class CorrectDatabase + * @codeCoverageIgnore */ class CorrectDatabase extends Command { @@ -38,7 +39,7 @@ class CorrectDatabase extends Command * * @var string */ - protected $description = 'Will correct the integrity of your database, of necessary.'; + protected $description = 'Will correct the integrity of your database, if necessary.'; /** * The name and signature of the console command. * diff --git a/app/Console/Commands/Correction/CreateAccessTokens.php b/app/Console/Commands/Correction/CreateAccessTokens.php index f14632553f..9d2ce023dd 100644 --- a/app/Console/Commands/Correction/CreateAccessTokens.php +++ b/app/Console/Commands/Correction/CreateAccessTokens.php @@ -22,6 +22,7 @@ namespace FireflyIII\Console\Commands\Correction; use Exception; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; @@ -35,7 +36,7 @@ class CreateAccessTokens extends Command * * @var string */ - protected $description = 'Creates user access tokens.'; + protected $description = 'Creates user access tokens which are used for command line access to personal data.'; /** * The name and signature of the console command. * @@ -51,9 +52,13 @@ class CreateAccessTokens extends Command */ public function handle(): int { + // make repository: + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + $start = microtime(true); $count = 0; - $users = User::get(); + $users= $repository->all(); /** @var User $user */ foreach ($users as $user) { $pref = app('preferences')->getForUser($user, 'access_token', null); diff --git a/app/Console/Commands/Correction/CreateLinkTypes.php b/app/Console/Commands/Correction/CreateLinkTypes.php index 6cc158af3e..dfb9ade9b6 100644 --- a/app/Console/Commands/Correction/CreateLinkTypes.php +++ b/app/Console/Commands/Correction/CreateLinkTypes.php @@ -58,12 +58,13 @@ class CreateLinkTypes extends Command 'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'], ]; foreach ($set as $name => $values) { - $link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first(); + $link = LinkType::where('name', $name) + ->first(); if (null === $link) { $link = new LinkType; $link->name = $name; - $link->outward = $values[0]; $link->inward = $values[1]; + $link->outward = $values[0]; ++$count; $this->line(sprintf('Created missing link type "%s"', $name)); } diff --git a/app/Console/Commands/Correction/DeleteEmptyGroups.php b/app/Console/Commands/Correction/DeleteEmptyGroups.php index 98887142e9..4db1945866 100644 --- a/app/Console/Commands/Correction/DeleteEmptyGroups.php +++ b/app/Console/Commands/Correction/DeleteEmptyGroups.php @@ -59,7 +59,7 @@ class DeleteEmptyGroups extends Command $this->info('No empty transaction groups.'); } if ($count > 0) { - $this->info(sprintf('Deleted %d empty transaction groups.', $count)); + $this->info(sprintf('Deleted %d empty transaction group(s).', $count)); TransactionGroup::whereNull('deleted_at')->whereNotIn('id', $groups)->delete(); } $end = round(microtime(true) - $start, 2); diff --git a/app/Console/Commands/Correction/DeleteEmptyJournals.php b/app/Console/Commands/Correction/DeleteEmptyJournals.php index dad50592ef..004c1f91ea 100644 --- a/app/Console/Commands/Correction/DeleteEmptyJournals.php +++ b/app/Console/Commands/Correction/DeleteEmptyJournals.php @@ -72,9 +72,12 @@ class DeleteEmptyJournals extends Command foreach ($set as $entry) { try { TransactionJournal::find($entry->id)->delete(); + // @codeCoverageIgnoreStart } catch (Exception $e) { Log::info(sprintf('Could not delete entry: %s', $e->getMessage())); } + // @codeCoverageIgnoreEnd + $this->info(sprintf('Deleted empty transaction journal #%d', $entry->id)); ++$count; } @@ -90,15 +93,6 @@ class DeleteEmptyJournals extends Command */ private function deleteUnevenJournals(): void { - /** - * select count(transactions.transaction_journal_id) as the_count, transactions.transaction_journal_id from transactions - * - * where transactions.deleted_at is null - * - * group by transactions.transaction_journal_id - * having the_count in () - */ - $set = Transaction ::whereNull('deleted_at') ->having('the_count', '!=', '2') @@ -111,9 +105,12 @@ class DeleteEmptyJournals extends Command // uneven number, delete journal and transactions: try { TransactionJournal::find((int)$row->transaction_journal_id)->delete(); + // @codeCoverageIgnoreStart } catch(Exception $e) { Log::info(sprintf('Could not delete journal: %s', $e->getMessage())); } + // @codeCoverageIgnoreEnd + Transaction::where('transaction_journal_id', (int)$row->transaction_journal_id)->delete(); $this->info(sprintf('Deleted transaction journal #%d because it had an uneven number of transactions.', $row->transaction_journal_id)); $total++; diff --git a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php index 1cd267b9c8..7b433ba3f4 100644 --- a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php +++ b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php @@ -25,8 +25,8 @@ use Exception; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -use stdClass; use Log; +use stdClass; /** * Deletes transactions where the journal has been deleted. @@ -81,17 +81,20 @@ class DeleteOrphanedTransactions extends Command if ($journal) { try { $journal->delete(); + // @codeCoverageIgnoreStart } catch (Exception $e) { - Log::info(sprintf('Could not delete transaction %s', $e->getMessage())); + Log::info(sprintf('Could not delete journal %s', $e->getMessage())); } + // @codeCoverageIgnoreEnd } Transaction::where('transaction_journal_id', (int)$transaction->transaction_journal_id)->delete(); $this->line( - sprintf('Deleted transaction journal #%d because account #%d was already deleted.', $transaction->transaction_journal_id, $transaction->account_id) + sprintf('Deleted transaction journal #%d because account #%d was already deleted.', + $transaction->transaction_journal_id, $transaction->account_id) ); $count++; } - if(0===$count) { + if (0 === $count) { $this->info('No orphaned accounts.'); } } diff --git a/app/Console/Commands/Correction/DeleteZeroAmount.php b/app/Console/Commands/Correction/DeleteZeroAmount.php index 6c062f74de..e55b413f1d 100644 --- a/app/Console/Commands/Correction/DeleteZeroAmount.php +++ b/app/Console/Commands/Correction/DeleteZeroAmount.php @@ -47,7 +47,6 @@ class DeleteZeroAmount extends Command /** * Execute the console command. - * @throws Exception * @return int */ public function handle(): int @@ -60,7 +59,13 @@ class DeleteZeroAmount extends Command /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $this->info(sprintf('Deleted transaction journal #%d because the amount is zero (0.00).', $journal->id)); - $journal->delete(); + try { + $journal->delete(); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + $this->line($e->getMessage()); + } + // @codeCoverageIgnoreEnd Transaction::where('transaction_journal_id', $journal->id)->delete(); } if (0 === $journals->count()) { diff --git a/app/Console/Commands/Correction/EnableCurrencies.php b/app/Console/Commands/Correction/EnableCurrencies.php index e4f7bfd395..acfb43f05a 100644 --- a/app/Console/Commands/Correction/EnableCurrencies.php +++ b/app/Console/Commands/Correction/EnableCurrencies.php @@ -86,9 +86,13 @@ class EnableCurrencies extends Command $found = array_unique($found); $this->info(sprintf('%d different currencies are currently in use.', count($found))); + $disabled = TransactionCurrency::whereIn('id', $found)->where('enabled', false)->count(); if ($disabled > 0) { - $this->info(sprintf('%d were still disabled. This has been corrected.', $disabled)); + $this->info(sprintf('%d were (was) still disabled. This has been corrected.', $disabled)); + } + if (0 === $disabled) { + $this->info('All currencies are correctly enabled or disabled.'); } TransactionCurrency::whereIn('id', $found)->update(['enabled' => true]); diff --git a/app/Console/Commands/Correction/FixAccountTypes.php b/app/Console/Commands/Correction/FixAccountTypes.php index c776e74d8c..ed936a1900 100644 --- a/app/Console/Commands/Correction/FixAccountTypes.php +++ b/app/Console/Commands/Correction/FixAccountTypes.php @@ -52,82 +52,16 @@ class FixAccountTypes extends Command private $factory; /** @var array */ private $fixable; + /** @var int */ + private $count; /** - * @param TransactionJournal $journal - * @param string $type - * @param Transaction $source - * @param Transaction $dest - * @throws FireflyException + * FixAccountTypes constructor. */ - public function fixJournal(TransactionJournal $journal, string $type, Transaction $source, Transaction $dest): void + public function __construct() { - // variables: - $combination = sprintf('%s%s%s', $type, $source->account->accountType->type, $dest->account->accountType->type); - - switch ($combination) { - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::LOAN): - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::DEBT): - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::MORTGAGE): - // from an asset to a liability should be a withdrawal: - $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); - $journal->transactionType()->associate($withdrawal); - $journal->save(); - $this->info(sprintf('Converted transaction #%d from a transfer to a withdrawal', $journal->id)); - // check it again: - $this->inspectJournal($journal); - break; - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::LOAN, AccountType::ASSET): - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::DEBT, AccountType::ASSET): - case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::MORTGAGE, AccountType::ASSET): - // from a liability to an asset should be a deposit. - $deposit = TransactionType::whereType(TransactionType::DEPOSIT)->first(); - $journal->transactionType()->associate($deposit); - $journal->save(); - $this->info(sprintf('Converted transaction #%d from a transfer to a deposit', $journal->id)); - // check it again: - $this->inspectJournal($journal); - - break; - case sprintf('%s%s%s', TransactionType::WITHDRAWAL, AccountType::ASSET, AccountType::REVENUE): - // withdrawals with a revenue account as destination instead of an expense account. - $this->factory->setUser($journal->user); - $result = $this->factory->findOrCreate($source->account->name, AccountType::EXPENSE); - $dest->account()->associate($result); - $dest->save(); - $this->info( - sprintf( - 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', $journal->id, - $source->account->id, $source->account->name, - $result->id, $result->name - ) - ); - $this->inspectJournal($journal); - break; - case sprintf('%s%s%s', TransactionType::DEPOSIT, AccountType::EXPENSE, AccountType::ASSET): - // deposits with an expense account as source instead of a revenue account. - // find revenue account. - $this->factory->setUser($journal->user); - $result = $this->factory->findOrCreate($source->account->name, AccountType::REVENUE); - $source->account()->associate($result); - $source->save(); - $this->info( - sprintf( - 'Transaction journal #%d, source account changed from #%d ("%s") to #%d ("%s").', $journal->id, - $source->account->id, $source->account->name, - $result->id, $result->name - ) - ); - $this->inspectJournal($journal); - break; - break; - default: - $this->info(sprintf('The source account of %s #%d cannot be of type "%s".', $type, $journal->id, $source->account->accountType->type)); - $this->info(sprintf('The destination account of %s #%d cannot be of type "%s".', $type, $journal->id, $dest->account->accountType->type)); - break; - - } - + parent::__construct(); + $this->count = 0; } /** @@ -159,19 +93,103 @@ class FixAccountTypes extends Command $this->expected = config('firefly.source_dests'); - $journals = TransactionJournal - - ::with(['TransactionType', 'transactions', 'transactions.account', 'transactions.account.accounttype'])->get(); + $journals = TransactionJournal::with(['TransactionType', 'transactions', 'transactions.account', 'transactions.account.accounttype'])->get(); foreach ($journals as $journal) { $this->inspectJournal($journal); } + if (0 === $this->count) { + $this->info('All account types are OK!'); + } + if (0 !== $this->count) { + $this->info(sprintf('Acted on %d transaction(s)!', $this->count)); + } $end = round(microtime(true) - $start, 2); - $this->info(sprintf('Verified account types in %s seconds', $end)); + $this->info(sprintf('Verifying account types took %s seconds', $end)); return 0; } + /** + * @param TransactionJournal $journal + * @param string $type + * @param Transaction $source + * @param Transaction $dest + * @throws FireflyException + */ + private function fixJournal(TransactionJournal $journal, string $type, Transaction $source, Transaction $dest): void + { + $this->count++; + // variables: + $combination = sprintf('%s%s%s', $type, $source->account->accountType->type, $dest->account->accountType->type); + + switch ($combination) { + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::LOAN): + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::DEBT): + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::ASSET, AccountType::MORTGAGE): + // from an asset to a liability should be a withdrawal: + $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $journal->transactionType()->associate($withdrawal); + $journal->save(); + $this->info(sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id)); + // check it again: + $this->inspectJournal($journal); + break; + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::LOAN, AccountType::ASSET): + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::DEBT, AccountType::ASSET): + case sprintf('%s%s%s', TransactionType::TRANSFER, AccountType::MORTGAGE, AccountType::ASSET): + // from a liability to an asset should be a deposit. + $deposit = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + $journal->transactionType()->associate($deposit); + $journal->save(); + $this->info(sprintf('Converted transaction #%d from a transfer to a deposit.', $journal->id)); + // check it again: + $this->inspectJournal($journal); + + break; + case sprintf('%s%s%s', TransactionType::WITHDRAWAL, AccountType::ASSET, AccountType::REVENUE): + // withdrawals with a revenue account as destination instead of an expense account. + $this->factory->setUser($journal->user); + $oldDest = $dest->account; + $result = $this->factory->findOrCreate($dest->account->name, AccountType::EXPENSE); + $dest->account()->associate($result); + $dest->save(); + $this->info( + sprintf( + 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', $journal->id, + $oldDest->id, $oldDest->name, + $result->id, $result->name + ) + ); + $this->inspectJournal($journal); + break; + case sprintf('%s%s%s', TransactionType::DEPOSIT, AccountType::EXPENSE, AccountType::ASSET): + // deposits with an expense account as source instead of a revenue account. + // find revenue account. + $this->factory->setUser($journal->user); + $result = $this->factory->findOrCreate($source->account->name, AccountType::REVENUE); + $oldSource = $dest->account; + $source->account()->associate($result); + $source->save(); + $this->info( + sprintf( + 'Transaction journal #%d, source account changed from #%d ("%s") to #%d ("%s").', $journal->id, + $oldSource->id, $oldSource->name, + $result->id, $result->name + ) + ); + $this->inspectJournal($journal); + break; + default: + $this->info(sprintf('The source account of %s #%d cannot be of type "%s".', $type, $journal->id, $source->account->accountType->type)); + $this->info(sprintf('The destination account of %s #%d cannot be of type "%s".', $type, $journal->id, $dest->account->accountType->type)); + + break; + + } + + } + /** * @param TransactionJournal $journal * @@ -201,7 +219,7 @@ class FixAccountTypes extends Command { $count = $journal->transactions()->count(); if (2 !== $count) { - $this->info(sprintf('Cannot inspect transaction journal #%d because it has %d transactions instead of 2.', $journal->id, $count)); + $this->info(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $count)); return; } @@ -213,9 +231,11 @@ class FixAccountTypes extends Command $destAccount = $destTransaction->account; $destAccountType = $destAccount->accountType->type; if (!isset($this->expected[$type])) { + // @codeCoverageIgnoreStart $this->info(sprintf('No source/destination info for transaction type %s.', $type)); return; + // @codeCoverageIgnoreEnd } if (!isset($this->expected[$type][$sourceAccountType])) { $this->fixJournal($journal, $type, $sourceTransaction, $destTransaction); diff --git a/app/Console/Commands/Correction/FixPiggies.php b/app/Console/Commands/Correction/FixPiggies.php index c7701747b5..5cfcd81571 100644 --- a/app/Console/Commands/Correction/FixPiggies.php +++ b/app/Console/Commands/Correction/FixPiggies.php @@ -46,6 +46,9 @@ class FixPiggies extends Command */ protected $signature = 'firefly-iii:fix-piggies'; + /** @var int */ + private $count; + /** * Execute the console command. * @@ -53,29 +56,43 @@ class FixPiggies extends Command */ public function handle(): int { - $start = microtime(true); - $set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get(); - $set->each( - function (PiggyBankEvent $event) { - if (null === $event->transaction_journal_id) { - return true; - } - /** @var TransactionJournal $journal */ - $journal = $event->transactionJournal()->first(); - if (null === $journal) { - return true; - } + $this->count = 0; + $start = microtime(true); + $set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get(); - $type = $journal->transactionType->type; - if (TransactionType::TRANSFER !== $type) { - $event->transaction_journal_id = null; - $event->save(); - $this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id)); - } + /** @var PiggyBankEvent $event */ + foreach ($set as $event) { - return true; + if (null === $event->transaction_journal_id) { + continue; } - ); + /** @var TransactionJournal $journal */ + $journal = $event->transactionJournal; + // @codeCoverageIgnoreStart + if (null === $journal) { + $event->transaction_journal_id = null; + $event->save(); + $this->count++; + continue; + } + // @codeCoverageIgnoreEnd + + $type = $journal->transactionType->type; + if (TransactionType::TRANSFER !== $type) { + $event->transaction_journal_id = null; + $event->save(); + $this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id)); + $this->count++; + continue; + } + } + if (0 === $this->count) { + $this->line('All piggy bank events are correct.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Fixed %d piggy bank event(s).', $this->count)); + } + $end = round(microtime(true) - $start, 2); $this->line(sprintf('Verified the content of %d piggy bank events in %s seconds.', $set->count(), $end)); diff --git a/app/Console/Commands/Correction/FixUnevenAmount.php b/app/Console/Commands/Correction/FixUnevenAmount.php index 5a915cc457..7b72aeef8b 100644 --- a/app/Console/Commands/Correction/FixUnevenAmount.php +++ b/app/Console/Commands/Correction/FixUnevenAmount.php @@ -22,6 +22,7 @@ namespace FireflyIII\Console\Commands\Correction; use DB; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; @@ -84,7 +85,7 @@ class FixUnevenAmount extends Command // one of the transactions is bad. $journal = TransactionJournal::find($param); if (!$journal) { - return; + return; // @codeCoverageIgnore } /** @var Transaction $source */ $source = $journal->transactions()->where('amount', '<', 0)->first(); @@ -92,11 +93,11 @@ class FixUnevenAmount extends Command // fix amount of destination: /** @var Transaction $destination */ - $destination = $journal->transactions()->where('amount', '>', 0)->first(); + $destination = $journal->transactions()->where('amount', '>', 0)->first(); $destination->amount = $amount; $destination->save(); - $this->line(sprintf('Corrected amount in transaction journal #%d', $param)); - + $message = sprintf('Corrected amount in transaction journal #%d', $param); + $this->line($message); } } diff --git a/app/Console/Commands/Correction/RenameMetaFields.php b/app/Console/Commands/Correction/RenameMetaFields.php index 4b4656d613..5758376a9c 100644 --- a/app/Console/Commands/Correction/RenameMetaFields.php +++ b/app/Console/Commands/Correction/RenameMetaFields.php @@ -42,6 +42,9 @@ class RenameMetaFields extends Command */ protected $signature = 'firefly-iii:rename-meta-fields'; + /** @var int */ + private $count; + /** * Execute the console command. * @@ -49,7 +52,8 @@ class RenameMetaFields extends Command */ public function handle(): int { - $start = microtime(true); + $this->count = 0; + $start = microtime(true); $changes = [ 'original-source' => 'original_source', @@ -67,6 +71,12 @@ class RenameMetaFields extends Command foreach ($changes as $original => $update) { $this->rename($original, $update); } + if (0 === $this->count) { + $this->line('All meta fields are correct.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Renamed %d meta field(s).', $this->count)); + } $end = round(microtime(true) - $start, 2); $this->info(sprintf('Renamed meta fields in %s seconds', $end)); @@ -80,8 +90,9 @@ class RenameMetaFields extends Command */ private function rename(string $original, string $update): void { - DB::table('journal_meta') - ->where('name', '=', $original) - ->update(['name' => $update]); + $count = DB::table('journal_meta') + ->where('name', '=', $original) + ->update(['name' => $update]); + $this->count += $count; } } diff --git a/app/Console/Commands/Correction/TransferBudgets.php b/app/Console/Commands/Correction/TransferBudgets.php index 9b44827024..8598acc6fc 100644 --- a/app/Console/Commands/Correction/TransferBudgets.php +++ b/app/Console/Commands/Correction/TransferBudgets.php @@ -66,6 +66,9 @@ class TransferBudgets extends Command if (0 === $count) { $this->info('No invalid budget/journal entries.'); } + if(0 !== $count) { + $this->line(sprintf('Corrected %d invalid budget/journal entries (entry).', $count)); + } $end = round(microtime(true) - $start, 2); $this->info(sprintf('Verified budget/journals in %s seconds.', $end)); diff --git a/app/Console/Commands/DecryptDatabase.php b/app/Console/Commands/DecryptDatabase.php index 21cd995db6..437226fe2e 100644 --- a/app/Console/Commands/DecryptDatabase.php +++ b/app/Console/Commands/DecryptDatabase.php @@ -49,15 +49,15 @@ class DecryptDatabase extends Command * * @var string */ - protected $signature = 'firefly:decrypt-all'; + protected $signature = 'firefly-iii:decrypt-all'; /** * Execute the console command. * - * @return mixed + * @return int * @throws FireflyException */ - public function handle() + public function handle(): int { $this->line('Going to decrypt the database.'); $tables = [ @@ -151,7 +151,7 @@ class DecryptDatabase extends Command $value = Crypt::decrypt($value); } catch (DecryptException $e) { if ('The MAC is invalid.' === $e->getMessage()) { - throw new FireflyException($e->getMessage()); + throw new FireflyException($e->getMessage()); // @codeCoverageIgnore } Log::debug(sprintf('Could not decrypt. %s', $e->getMessage())); } diff --git a/app/Console/Commands/Import/CreateCSVImport.php b/app/Console/Commands/Import/CreateCSVImport.php index cae2732402..ad64c742db 100644 --- a/app/Console/Commands/Import/CreateCSVImport.php +++ b/app/Console/Commands/Import/CreateCSVImport.php @@ -28,18 +28,17 @@ namespace FireflyIII\Console\Commands\Import; use Exception; use FireflyIII\Console\Commands\VerifiesAccessToken; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Import\Prerequisites\PrerequisitesInterface; use FireflyIII\Import\Routine\RoutineInterface; use FireflyIII\Import\Storage\ImportArrayStorage; +use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\User; use Illuminate\Console\Command; use Log; /** * Class CreateCSVImport. - * - * @codeCoverageIgnore */ class CreateCSVImport extends Command { @@ -50,7 +49,6 @@ class CreateCSVImport extends Command * @var string */ protected $description = 'Use this command to create a new CSV file import.'; - /** * The name and signature of the console command. * @@ -62,169 +60,96 @@ class CreateCSVImport extends Command {configuration? : The configuration file to use for the import.} {--user=1 : The user ID that the import should import for.} {--token= : The user\'s access token.}'; + /** @var UserRepositoryInterface */ + private $userRepository; + /** @var ImportJobRepositoryInterface */ + private $importRepository; + /** @var ImportJob */ + private $importJob; + + /** + * CreateCSVImport constructor. + */ + public function __construct() + { + parent::__construct(); + $this->userRepository = app(UserRepositoryInterface::class); + $this->importRepository = app(ImportJobRepositoryInterface::class); + + } /** * Run the command. - * - * @throws FireflyException */ public function handle(): int { + // @codeCoverageIgnoreStart if (!$this->verifyAccessToken()) { $this->errorLine('Invalid access token.'); return 1; } - /** @var UserRepositoryInterface $userRepository */ - $userRepository = app(UserRepositoryInterface::class); - $file = (string)$this->argument('file'); - $configuration = (string)$this->argument('configuration'); - $user = $userRepository->findNull((int)$this->option('user')); - $cwd = getcwd(); - $configurationData = []; - - if (null === $user) { - $this->errorLine('User is NULL.'); - - return 1; - } if (!$this->validArguments()) { $this->errorLine('Invalid arguments.'); return 1; } - if ('' !== $configuration) { - $configurationData = json_decode(file_get_contents($configuration), true); - if (null === $configurationData) { - $this->errorLine(sprintf('Firefly III cannot read the contents of configuration file "%s" (working directory: "%s").', $configuration, $cwd)); + // @codeCoverageIgnoreEnd + /** @var User $user */ + $user = $this->userRepository->findNull((int)$this->option('user')); + $file = (string)$this->argument('file'); + $configuration = (string)$this->argument('configuration'); - return 1; - } + $this->importRepository->setUser($user); + + $configurationData = json_decode(file_get_contents($configuration), true); + $this->importJob = $this->importRepository->create('file'); + + + // inform user (and log it) + $this->infoLine(sprintf('Import file : %s', $file)); + $this->infoLine(sprintf('Configuration file : %s', $configuration)); + $this->infoLine(sprintf('User : #%d (%s)', $user->id, $user->email)); + $this->infoLine(sprintf('Job : %s', $this->importJob->key)); + + try { + $this->storeFile($file); + } catch (FireflyException $e) { + $this->errorLine($e->getMessage()); + + return 1; } - - $this->infoLine(sprintf('Going to create a job to import file: %s', $file)); - $this->infoLine(sprintf('Using configuration file: %s', $configuration)); - $this->infoLine(sprintf('Import into user: #%d (%s)', $user->id, $user->email)); - - /** @var ImportJobRepositoryInterface $jobRepository */ - $jobRepository = app(ImportJobRepositoryInterface::class); - $jobRepository->setUser($user); - $importJob = $jobRepository->create('file'); - $this->infoLine(sprintf('Created job "%s"', $importJob->key)); - - // make sure that job has no prerequisites. - if ((bool)config('import.has_prereq.csv')) { - // make prerequisites thing. - $class = (string)config('import.prerequisites.csv'); - if (!class_exists($class)) { - throw new FireflyException('No class to handle prerequisites for CSV.'); // @codeCoverageIgnore - } - /** @var PrerequisitesInterface $object */ - $object = app($class); - $object->setUser($user); - if (!$object->isComplete()) { - $this->errorLine('CSV Import provider has prerequisites that can only be filled in using the browser.'); - - return 1; - } - } - - // store file as attachment. - if ('' !== $file) { - $messages = $jobRepository->storeCLIUpload($importJob, 'import_file', $file); - if ($messages->count() > 0) { - $this->errorLine($messages->first()); - - return 1; - } - $this->infoLine('File content saved.'); - } - - $this->infoLine('Job configuration saved.'); - $jobRepository->setConfiguration($importJob, $configurationData); - $jobRepository->setStatus($importJob, 'ready_to_run'); - + // job is ready to go + $this->importRepository->setConfiguration($this->importJob, $configurationData); + $this->importRepository->setStatus($this->importJob, 'ready_to_run'); $this->infoLine('The import routine has started. The process is not visible. Please wait.'); Log::debug('Go for import!'); - // run it! - $className = config('import.routine.file'); - if (null === $className || !class_exists($className)) { - // @codeCoverageIgnoreStart - $this->errorLine('No routine for file provider.'); - - return 1; - // @codeCoverageIgnoreEnd - } // keep repeating this call until job lands on "provider_finished" - $valid = ['provider_finished']; - $count = 0; - while (!in_array($importJob->status, $valid, true) && $count < 6) { - Log::debug(sprintf('Now in loop #%d.', $count + 1)); - /** @var RoutineInterface $routine */ - $routine = app($className); - $routine->setImportJob($importJob); - try { - $routine->run(); - } catch (FireflyException|Exception $e) { - $message = 'The import routine crashed: ' . $e->getMessage(); - Log::error($message); - Log::error($e->getTraceAsString()); + try { + $this->processFile(); + } catch (FireflyException $e) { + $this->errorLine($e->getMessage()); - // set job errored out: - $jobRepository->setStatus($importJob, 'error'); - $this->errorLine($message); - - return 1; - } - $count++; + return 1; } - if ('provider_finished' === $importJob->status) { - $this->infoLine('Import has finished. Please wait for storage of data.'); - // set job to be storing data: - $jobRepository->setStatus($importJob, 'storing_data'); - /** @var ImportArrayStorage $storage */ - $storage = app(ImportArrayStorage::class); - $storage->setImportJob($importJob); + // then store data: + try { + $this->storeData(); + } catch (FireflyException $e) { + $this->errorLine($e->getMessage()); - try { - $storage->store(); - } catch (FireflyException|Exception $e) { - $message = 'The import routine crashed: ' . $e->getMessage(); - Log::error($message); - Log::error($e->getTraceAsString()); - - // set job errored out: - $jobRepository->setStatus($importJob, 'error'); - $this->errorLine($message); - - return 1; - } - // set storage to be finished: - $jobRepository->setStatus($importJob, 'storage_finished'); + return 1; } // give feedback: - $this->infoLine('Job has finished.'); - if (null !== $importJob->tag) { - $this->infoLine(sprintf('%d transaction(s) have been imported.', $importJob->tag->transactionJournals->count())); - $this->infoLine(sprintf('You can find your transactions under tag "%s"', $importJob->tag->tag)); - } + $this->giveFeedback(); - if (null === $importJob->tag) { - $this->errorLine('No transactions have been imported :(.'); - } - if (count($importJob->errors) > 0) { - $this->infoLine(sprintf('%d error(s) occurred:', count($importJob->errors))); - foreach ($importJob->errors as $err) { - $this->errorLine('- ' . $err); - } - } // clear cache for user: app('preferences')->setForUser($user, 'lastActivity', microtime()); @@ -234,6 +159,7 @@ class CreateCSVImport extends Command /** * @param string $message * @param array|null $data + * @codeCoverageIgnore */ private function errorLine(string $message, array $data = null): void { @@ -245,6 +171,7 @@ class CreateCSVImport extends Command /** * @param string $message * @param array $data + * @codeCoverageIgnore */ private function infoLine(string $message, array $data = null): void { @@ -257,6 +184,7 @@ class CreateCSVImport extends Command * * @noinspection MultipleReturnStatementsInspection * @return bool + * @codeCoverageIgnore */ private function validArguments(): bool { @@ -283,6 +211,119 @@ class CreateCSVImport extends Command return false; } + $configurationData = json_decode(file_get_contents($configuration), true); + if (null === $configurationData) { + $this->errorLine(sprintf('Firefly III cannot read the contents of configuration file "%s" (working directory: "%s").', $configuration, $cwd)); + + return false; + } + return true; } + + /** + * Store the supplied file as an attachment to this job. + * + * @param string $file + * @throws FireflyException + */ + private function storeFile(string $file): void + { + // store file as attachment. + if ('' !== $file) { + $messages = $this->importRepository->storeCLIUpload($this->importJob, 'import_file', $file); + if ($messages->count() > 0) { + throw new FireflyException($messages->first()); + } + } + } + + /** + * Keep repeating import call until job lands on "provider_finished". + * + * @throws FireflyException + */ + private function processFile(): void + { + $className = config('import.routine.file'); + $valid = ['provider_finished']; + $count = 0; + + while (!in_array($this->importJob->status, $valid, true) && $count < 6) { + Log::debug(sprintf('Now in loop #%d.', $count + 1)); + /** @var RoutineInterface $routine */ + $routine = app($className); + $routine->setImportJob($this->importJob); + try { + $routine->run(); + } catch (FireflyException|Exception $e) { + $message = 'The import routine crashed: ' . $e->getMessage(); + Log::error($message); + Log::error($e->getTraceAsString()); + + // set job errored out: + $this->importRepository->setStatus($this->importJob, 'error'); + throw new FireflyException($message); + } + $count++; + } + $this->importRepository->setStatus($this->importJob, 'provider_finished'); + $this->importJob->status = 'provider_finished'; + } + + /** + * + * @throws FireflyException + */ + private function storeData(): void + { + if ('provider_finished' === $this->importJob->status) { + $this->infoLine('Import has finished. Please wait for storage of data.'); + // set job to be storing data: + $this->importRepository->setStatus($this->importJob, 'storing_data'); + + /** @var ImportArrayStorage $storage */ + $storage = app(ImportArrayStorage::class); + $storage->setImportJob($this->importJob); + + try { + $storage->store(); + } catch (FireflyException|Exception $e) { + $message = 'The import routine crashed: ' . $e->getMessage(); + Log::error($message); + Log::error($e->getTraceAsString()); + + // set job errored out: + $this->importRepository->setStatus($this->importJob, 'error'); + throw new FireflyException($message); + + } + // set storage to be finished: + $this->importRepository->setStatus($this->importJob, 'storage_finished'); + } + } + + /** + * + */ + private function giveFeedback(): void + { + $this->infoLine('Job has finished.'); + + + if (null !== $this->importJob->tag) { + $this->infoLine(sprintf('%d transaction(s) have been imported.', $this->importJob->tag->transactionJournals->count())); + $this->infoLine(sprintf('You can find your transactions under tag "%s"', $this->importJob->tag->tag)); + } + + if (null === $this->importJob->tag) { + $this->errorLine('No transactions have been imported :(.'); + } + if (count($this->importJob->errors) > 0) { + $this->infoLine(sprintf('%d error(s) occurred:', count($this->importJob->errors))); + foreach ($this->importJob->errors as $err) { + $this->errorLine('- ' . $err); + } + } + } } diff --git a/app/Console/Commands/Integrity/ReportEmptyObjects.php b/app/Console/Commands/Integrity/ReportEmptyObjects.php index ec5dc922ec..69ab4b8848 100644 --- a/app/Console/Commands/Integrity/ReportEmptyObjects.php +++ b/app/Console/Commands/Integrity/ReportEmptyObjects.php @@ -80,9 +80,8 @@ class ReportEmptyObjects extends Command /** @var stdClass $entry */ foreach ($set as $entry) { - $name = $entry->name; $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.'; - $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name); + $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name); $this->line($line); } } @@ -125,13 +124,12 @@ class ReportEmptyObjects extends Command /** @var stdClass $entry */ foreach ($set as $entry) { - $objName = $entry->name; $line = sprintf( 'User #%d (%s) has budget #%d ("%s") which has no transaction journals.', $entry->user_id, $entry->email, $entry->id, - $objName + $entry->name ); $this->line($line); } @@ -151,14 +149,12 @@ class ReportEmptyObjects extends Command /** @var stdClass $entry */ foreach ($set as $entry) { - $objName = $entry->name; - $line = sprintf( 'User #%d (%s) has category #%d ("%s") which has no transaction journals.', $entry->user_id, $entry->email, $entry->id, - $objName + $entry->name ); $this->line($line); } @@ -178,14 +174,13 @@ class ReportEmptyObjects extends Command /** @var stdClass $entry */ foreach ($set as $entry) { - $objName = $entry->tag; $line = sprintf( 'User #%d (%s) has tag #%d ("%s") which has no transaction journals.', $entry->user_id, $entry->email, $entry->id, - $objName + $entry->tag ); $this->line($line); } diff --git a/app/Console/Commands/Integrity/ReportIntegrity.php b/app/Console/Commands/Integrity/ReportIntegrity.php index 83ca43a7c7..541418c67e 100644 --- a/app/Console/Commands/Integrity/ReportIntegrity.php +++ b/app/Console/Commands/Integrity/ReportIntegrity.php @@ -30,6 +30,7 @@ use Artisan; /** * Class ReportIntegrity + * @codeCoverageIgnore */ class ReportIntegrity extends Command { diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index 6e7f92157d..3eb4019a85 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -75,8 +75,6 @@ class ApplyRules extends Command private $acceptedAccounts; /** @var Carbon */ private $endDate; - /** @var Collection */ - private $results; /** @var array */ private $ruleGroupSelection; /** @var array */ @@ -106,7 +104,6 @@ class ApplyRules extends Command $this->accounts = new Collection; $this->ruleSelection = []; $this->ruleGroupSelection = []; - $this->results = new Collection; $this->ruleRepository = app(RuleRepositoryInterface::class); $this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class); $this->acceptedAccounts = [AccountType::DEFAULT, AccountType::DEBT, AccountType::ASSET, AccountType::LOAN, AccountType::MORTGAGE]; @@ -121,11 +118,14 @@ class ApplyRules extends Command */ public function handle(): int { + // @codeCoverageIgnoreStart if (!$this->verifyAccessToken()) { $this->error('Invalid access token.'); return 1; } + // @codeCoverageIgnoreEnd + // set user: $this->ruleRepository->setUser($this->getUser()); $this->ruleGroupRepository->setUser($this->getUser()); @@ -141,24 +141,8 @@ class ApplyRules extends Command $this->grabAllRules(); // loop all groups and rules and indicate if they're included: - $count = 0; - $rulesToApply = []; - /** @var RuleGroup $group */ - foreach ($this->groups as $group) { - /** @var Rule $rule */ - foreach ($group->rules as $rule) { - // if in rule selection, or group in selection or all rules, it's included. - $test = $this->includeRule($rule, $group); - if (true === $test) { - Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title)); - $count++; - $rulesToApply[] = $rule->id; - } - if (false === $test) { - Log::debug(sprintf('Will not include rule #%d "%s"', $rule->id, $rule->title)); - } - } - } + $rulesToApply = $this->getRulesToApply(); + $count = count($rulesToApply); if (0 === $count) { $this->error('No rules or rule groups have been included.'); $this->warn('Make a selection using:'); @@ -167,7 +151,6 @@ class ApplyRules extends Command $this->warn(' --all_rules'); } - // get transactions from asset accounts. /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->getUser()); @@ -176,7 +159,7 @@ class ApplyRules extends Command $journals = $collector->getExtractedJournals(); // start running rules. - $this->line(sprintf('Will apply %d rules to %d transactions.', $count, count($journals))); + $this->line(sprintf('Will apply %d rule(s) to %d transaction(s).', $count, count($journals))); // start looping. /** @var RuleEngine $ruleEngine */ @@ -191,6 +174,7 @@ class ApplyRules extends Command Log::debug('Start of new journal.'); $ruleEngine->processJournalArray($journal); Log::debug('Done with all rules for this group + done with journal.'); + /** @noinspection DisconnectedForeachInstructionInspection */ $bar->advance(); } $this->line(''); @@ -212,16 +196,10 @@ class ApplyRules extends Command } // verify rule groups. - $result = $this->verifyInputRuleGroups(); - if (false === $result) { - return $result; - } + $this->verifyInputRuleGroups(); // verify rules. - $result = $this->verifyInputRules(); - if (false === $result) { - return $result; - } + $this->verifyInputRules(); $this->verifyInputDates(); @@ -243,11 +221,13 @@ class ApplyRules extends Command $finalList = new Collection; $accountList = explode(',', $accountString); + // @codeCoverageIgnoreStart if (0 === count($accountList)) { $this->error('Please use the --accounts option to indicate the accounts to apply rules to.'); return false; } + // @codeCoverageIgnoreEnd /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); @@ -284,11 +264,12 @@ class ApplyRules extends Command return true; } $ruleGroupList = explode(',', $ruleGroupString); - + // @codeCoverageIgnoreStart if (0 === count($ruleGroupList)) { // can be empty. return true; } + // @codeCoverageIgnoreEnd foreach ($ruleGroupList as $ruleGroupId) { $ruleGroup = $this->ruleGroupRepository->find((int)$ruleGroupId); if ($ruleGroup->active) { @@ -314,11 +295,14 @@ class ApplyRules extends Command } $ruleList = explode(',', $ruleString); + // @codeCoverageIgnoreStart if (0 === count($ruleList)) { // can be empty. return true; } + // @codeCoverageIgnoreEnd + foreach ($ruleList as $ruleId) { $rule = $this->ruleRepository->find((int)$ruleId); if (null !== $rule && $rule->active) { @@ -383,4 +367,27 @@ class ApplyRules extends Command in_array($rule->id, $this->ruleSelection, true) || $this->allRules; } + + /** + * @return array + */ + private function getRulesToApply(): array + { + $rulesToApply = []; + /** @var RuleGroup $group */ + foreach ($this->groups as $group) { + $rules = $this->ruleGroupRepository->getActiveStoreRules($group); + /** @var Rule $rule */ + foreach ($rules as $rule) { + // if in rule selection, or group in selection or all rules, it's included. + $test = $this->includeRule($rule, $group); + if (true === $test) { + Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title)); + $rulesToApply[] = $rule->id; + } + } + } + + return $rulesToApply; + } } diff --git a/app/Console/Commands/Upgrade/AccountCurrencies.php b/app/Console/Commands/Upgrade/AccountCurrencies.php index 4843162430..14ee979839 100644 --- a/app/Console/Commands/Upgrade/AccountCurrencies.php +++ b/app/Console/Commands/Upgrade/AccountCurrencies.php @@ -24,8 +24,10 @@ namespace FireflyIII\Console\Commands\Upgrade; use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; use Log; @@ -48,9 +50,23 @@ class AccountCurrencies extends Command * @var string */ protected $signature = 'firefly-iii:account-currencies {--F|force : Force the execution of this command.}'; - /** @var AccountRepositoryInterface */ - private $repository; + private $accountRepos; + /** @var UserRepositoryInterface */ + private $userRepos; + /** @var int */ + private $count; + + /** + * AccountCurrencies constructor. + */ + public function __construct() + { + parent::__construct(); + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->userRepos = app(UserRepositoryInterface::class); + $this->count = 0; + } /** * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account. @@ -59,8 +75,7 @@ class AccountCurrencies extends Command */ public function handle(): int { - $this->repository = app(AccountRepositoryInterface::class); - $start = microtime(true); + $start = microtime(true); if ($this->isExecuted() && true !== $this->option('force')) { $this->warn('This command has already been executed.'); @@ -69,11 +84,17 @@ class AccountCurrencies extends Command Log::debug('Now in updateAccountCurrencies()'); $this->updateAccountCurrencies(); + if (0 === $this->count) { + $this->line('All accounts are OK.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Corrected %d account(s).', $this->count)); + } + $end = round(microtime(true) - $start, 2); $this->info(sprintf('Verified and fixed account currencies in %s seconds.', $end)); $this->markAsExecuted(); - return 0; } @@ -100,41 +121,53 @@ class AccountCurrencies extends Command } /** - * @param Account $account + * @param Account $account * @param TransactionCurrency $currency */ private function updateAccount(Account $account, TransactionCurrency $currency): void { - $this->repository->setUser($account->user); - - $accountCurrency = (int)$this->repository->getMetaValue($account, 'currency_id'); - $openingBalance = $account->getOpeningBalance(); - $obCurrency = (int)$openingBalance->transaction_currency_id; + $this->accountRepos->setUser($account->user); + $accountCurrency = (int)$this->accountRepos->getMetaValue($account, 'currency_id'); + $openingBalance = $this->accountRepos->getOpeningBalance($account); + $obCurrency = 0; + if (null !== $openingBalance) { + $obCurrency = (int)$openingBalance->transaction_currency_id; + } // both 0? set to default currency: if (0 === $accountCurrency && 0 === $obCurrency) { AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete(); AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $currency->id]); $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $currency->code)); - + $this->count++; return; } // account is set to 0, opening balance is not? if (0 === $accountCurrency && $obCurrency > 0) { AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]); - $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $currency->code)); + $this->line(sprintf('Account #%d ("%s") now has a currency setting (#%d).', $account->id, $account->name, $obCurrency)); + $this->count++; return; } + // do not match and opening balance id is not null. - if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) { + if ($accountCurrency !== $obCurrency && null !== $openingBalance) { // update opening balance: $openingBalance->transaction_currency_id = $accountCurrency; $openingBalance->save(); + $openingBalance->transactions->each( + static function (Transaction $transaction) use ($accountCurrency) { + $transaction->transaction_currency_id = $accountCurrency; + $transaction->save(); + }); $this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); + $this->count++; + + return; } } @@ -143,24 +176,21 @@ class AccountCurrencies extends Command */ private function updateAccountCurrencies(): void { - + $users = $this->userRepos->all(); $defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR'); - $users = User::get(); foreach ($users as $user) { $this->updateCurrenciesForUser($user, $defaultCurrencyCode); } } /** - * @param User $user + * @param User $user * @param string $systemCurrencyCode */ private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void { - $accounts = $user->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET]) - ->get(['accounts.*']); + $this->accountRepos->setUser($user); + $accounts = $this->accountRepos->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); // get user's currency preference: $defaultCurrencyCode = app('preferences')->getForUser($user, 'currencyPreference', $systemCurrencyCode)->data; diff --git a/app/Console/Commands/Upgrade/BackToJournals.php b/app/Console/Commands/Upgrade/BackToJournals.php index 35ee45512d..5610e1751b 100644 --- a/app/Console/Commands/Upgrade/BackToJournals.php +++ b/app/Console/Commands/Upgrade/BackToJournals.php @@ -54,6 +54,7 @@ class BackToJournals extends Command */ public function handle(): int { + // @codeCoverageIgnoreStart $start = microtime(true); if (!$this->isMigrated()) { $this->error('Please run firefly-iii:migrate-to-groups first.'); @@ -66,6 +67,7 @@ class BackToJournals extends Command if (true === $this->option('force')) { $this->warn('Forcing the command.'); } + // @codeCoverageIgnoreEnd $this->migrateAll(); $end = round(microtime(true) - $start, 2); @@ -82,8 +84,9 @@ class BackToJournals extends Command { $transactions = DB::table('budget_transaction')->distinct()->get(['transaction_id'])->pluck('transaction_id')->toArray(); - return DB::table('transactions')->whereIn('transactions.id', $transactions)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray( - ); + return DB::table('transactions') + ->whereIn('transactions.id', $transactions) + ->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); } /** @@ -93,8 +96,9 @@ class BackToJournals extends Command { $transactions = DB::table('category_transaction')->distinct()->get(['transaction_id'])->pluck('transaction_id')->toArray(); - return DB::table('transactions')->whereIn('transactions.id', $transactions)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray( - ); + return DB::table('transactions') + ->whereIn('transactions.id', $transactions) + ->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); } /** @@ -149,9 +153,10 @@ class BackToJournals extends Command */ private function migrateBudgets(): void { + $journalIds = $this->getIdsForBudgets(); $journals = TransactionJournal::whereIn('id', $journalIds)->with(['transactions', 'budgets', 'transactions.budgets'])->get(); - $this->line(sprintf('Check %d transaction journals for budget info.',count($journals))); + $this->line(sprintf('Check %d transaction journal(s) for budget info.', count($journals))); /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $this->migrateBudgetsForJournal($journal); @@ -163,26 +168,34 @@ class BackToJournals extends Command */ private function migrateBudgetsForJournal(TransactionJournal $journal): void { + // grab category from first transaction /** @var Transaction $transaction */ $transaction = $journal->transactions->first(); if (null === $transaction) { + // @codeCoverageIgnoreStart $this->info(sprintf('Transaction journal #%d has no transactions. Will be fixed later.', $journal->id)); return; + // @codeCoverageIgnoreEnd } /** @var Budget $budget */ $budget = $transaction->budgets->first(); /** @var Budget $journalBudget */ $journalBudget = $journal->budgets->first(); + + // both have a budget, but they don't match. if (null !== $budget && null !== $journalBudget && $budget->id !== $journalBudget->id) { // sync to journal: $journal->budgets()->sync([(int)$budget->id]); + + return; } - // budget in transaction overrules journal. - if (null === $budget && null !== $journalBudget) { - $journal->budgets()->sync([]); + // transaction has a budget, but the journal doesn't. + if (null !== $budget && null === $journalBudget) { + // sync to journal: + $journal->budgets()->sync([(int)$budget->id]); } } @@ -193,7 +206,7 @@ class BackToJournals extends Command { $journalIds = $this->getIdsForCategories(); $journals = TransactionJournal::whereIn('id', $journalIds)->with(['transactions', 'categories', 'transactions.categories'])->get(); - $this->line(sprintf('Check %d transaction journals for category info.', count($journals))); + $this->line(sprintf('Check %d transaction journal(s) for category info.', count($journals))); /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $this->migrateCategoriesForJournal($journal); @@ -209,22 +222,26 @@ class BackToJournals extends Command /** @var Transaction $transaction */ $transaction = $journal->transactions->first(); if (null === $transaction) { + // @codeCoverageIgnoreStart $this->info(sprintf('Transaction journal #%d has no transactions. Will be fixed later.', $journal->id)); return; + // @codeCoverageIgnoreEnd } /** @var Category $category */ $category = $transaction->categories->first(); /** @var Category $journalCategory */ $journalCategory = $journal->categories->first(); + + // both have a category, but they don't match. if (null !== $category && null !== $journalCategory && $category->id !== $journalCategory->id) { // sync to journal: $journal->categories()->sync([(int)$category->id]); } - // category in transaction overrules journal. - if (null === $category && null !== $journalCategory) { - $journal->categories()->sync([]); + // transaction has a category, but the journal doesn't. + if (null !== $category && null === $journalCategory) { + $journal->categories()->sync([(int)$category->id]); } } } diff --git a/app/Console/Commands/Upgrade/BudgetLimitCurrency.php b/app/Console/Commands/Upgrade/BudgetLimitCurrency.php index c3205fdea0..99a0068d49 100644 --- a/app/Console/Commands/Upgrade/BudgetLimitCurrency.php +++ b/app/Console/Commands/Upgrade/BudgetLimitCurrency.php @@ -55,11 +55,14 @@ class BudgetLimitCurrency extends Command public function handle(): int { $start = microtime(true); + // @codeCoverageIgnoreStart if ($this->isExecuted() && true !== $this->option('force')) { $this->warn('This command has already been executed.'); return 0; } + // @codeCoverageIgnoreEnd + $count = 0; $budgetLimits = BudgetLimit::get(); /** @var BudgetLimit $budgetLimit */ diff --git a/app/Console/Commands/Upgrade/CCLiabilities.php b/app/Console/Commands/Upgrade/CCLiabilities.php index b6c29c266d..b09a172446 100644 --- a/app/Console/Commands/Upgrade/CCLiabilities.php +++ b/app/Console/Commands/Upgrade/CCLiabilities.php @@ -58,14 +58,19 @@ class CCLiabilities extends Command public function handle(): int { $start = microtime(true); + + // @codeCoverageIgnoreStart if ($this->isExecuted() && true !== $this->option('force')) { $this->warn('This command has already been executed.'); return 0; } + // @codeCoverageIgnoreEnd + $ccType = AccountType::where('type', AccountType::CREDITCARD)->first(); $debtType = AccountType::where('type', AccountType::DEBT)->first(); if (null === $ccType || null === $debtType) { + $this->info('No incorrectly stored credit card liabilities.'); return 0; } /** @var Collection $accounts */ diff --git a/app/Console/Commands/Upgrade/JournalCurrencies.php b/app/Console/Commands/Upgrade/JournalCurrencies.php index df4cbaa9fd..0c12eeef09 100644 --- a/app/Console/Commands/Upgrade/JournalCurrencies.php +++ b/app/Console/Commands/Upgrade/JournalCurrencies.php @@ -62,6 +62,20 @@ class JournalCurrencies extends Command private $currencyRepos; /** @var JournalRepositoryInterface */ private $journalRepos; + /** @var int */ + private $count; + + /** + * JournalCurrencies constructor. + */ + public function __construct() + { + parent::__construct(); + $this->count = 0; + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + $this->journalRepos = app(JournalRepositoryInterface::class); + } /** * Execute the console command. @@ -71,20 +85,27 @@ class JournalCurrencies extends Command public function handle(): int { $this->accountCurrencies = []; - $this->accountRepos = app(AccountRepositoryInterface::class); - $this->currencyRepos = app(CurrencyRepositoryInterface::class); - $this->journalRepos = app(JournalRepositoryInterface::class); + $start = microtime(true); + // @codeCoverageIgnoreStart if ($this->isExecuted() && true !== $this->option('force')) { $this->warn('This command has already been executed.'); return 0; } + // @codeCoverageIgnoreEnd $this->updateTransferCurrencies(); $this->updateOtherJournalsCurrencies(); $this->markAsExecuted(); + + if (0 === $this->count) { + $this->line('All transactions are correct.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Verified %d transaction(s) and journal(s).', $this->count)); + } $end = round(microtime(true) - $start, 2); $this->info(sprintf('Verified and fixed transaction currencies in %s seconds.', $end)); @@ -137,7 +158,8 @@ class JournalCurrencies extends Command private function getFirstAssetTransaction(TransactionJournal $journal): ?Transaction { $result = $journal->transactions->first( - function (Transaction $transaction) { + static function (Transaction $transaction) { + // type can also be liability. return AccountType::ASSET === $transaction->account->accountType->type; } ); @@ -180,7 +202,7 @@ class JournalCurrencies extends Command * This method makes sure that the transaction journal uses the currency given in the transaction. * * @param TransactionJournal $journal - * @param Transaction $transaction + * @param Transaction $transaction */ private function updateJournalCurrency(TransactionJournal $journal, Transaction $transaction): void { @@ -192,6 +214,7 @@ class JournalCurrencies extends Command } if (!((int)$currency->id === (int)$journal->transaction_currency_id)) { + $this->count++; $this->line( sprintf( 'Transfer #%d ("%s") has been updated to use %s instead of %s.', @@ -223,10 +246,11 @@ class JournalCurrencies extends Command } $journal->transactions->each( - function (Transaction $transaction) use ($currency) { + static function (Transaction $transaction) use ($currency) { if (null === $transaction->transaction_currency_id) { $transaction->transaction_currency_id = $currency->id; $transaction->save(); + $this->count++; } // when mismatch in transaction: @@ -235,11 +259,13 @@ class JournalCurrencies extends Command $transaction->foreign_amount = $transaction->amount; $transaction->transaction_currency_id = $currency->id; $transaction->save(); + $this->count++; } } ); // also update the journal, of course: $journal->transaction_currency_id = $currency->id; + $this->count++; $journal->save(); } @@ -255,11 +281,15 @@ class JournalCurrencies extends Command */ private function updateOtherJournalsCurrencies(): void { - $set = TransactionJournal - ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->whereNotIn('transaction_types.type', [TransactionType::TRANSFER]) - ->with(['transactions', 'transactions.account', 'transactions.account.accountType']) - ->get(['transaction_journals.*']); + $set = + $this->journalRepos->getAllJournals( + [ + TransactionType::WITHDRAWAL, + TransactionType::DEPOSIT, + TransactionType::OPENING_BALANCE, + TransactionType::RECONCILIATION, + ] + ); /** @var TransactionJournal $journal */ foreach ($set as $journal) { @@ -292,46 +322,54 @@ class JournalCurrencies extends Command $this->journalRepos->setUser($user); $this->currencyRepos->setUser($user); - $sourceAccountCurrency = $this->getCurrency($sourceAccount); - $destAccountCurrency = $this->getCurrency($destAccount); - if (null === $sourceAccountCurrency) { - Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $sourceAccount->id, $sourceAccount->name)); + $sourceCurrency = $this->getCurrency($sourceAccount); + $destCurrency = $this->getCurrency($destAccount); + if (null === $sourceCurrency) { + $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $sourceAccount->id, $sourceAccount->name); + Log::error($message); + $this->error($message); return; } // has no currency ID? Must have, so fill in using account preference: if (null === $source->transaction_currency_id) { - $source->transaction_currency_id = (int)$sourceAccountCurrency->id; - Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $source->id, $sourceAccountCurrency->code)); + $source->transaction_currency_id = (int)$sourceCurrency->id; + $message = sprintf('Transaction #%d has no currency setting, now set to %s.', $source->id, $sourceCurrency->code); + Log::debug($message); + $this->line($message); + $this->count++; $source->save(); } // does not match the source account (see above)? Can be fixed // when mismatch in transaction and NO foreign amount is set: - if (!((int)$source->transaction_currency_id === (int)$sourceAccountCurrency->id) && null === $source->foreign_amount) { - Log::debug( - sprintf( - 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', - $source->id, - $source->transaction_currency_id, - $sourceAccountCurrency->id, - $source->amount - ) + if (!((int)$source->transaction_currency_id === (int)$sourceCurrency->id) && null === $source->foreign_amount) { + $message = sprintf( + 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', + $source->id, + $source->transaction_currency_id, + $sourceCurrency->id, + $source->amount ); - $source->transaction_currency_id = (int)$sourceAccountCurrency->id; + Log::debug($message); + $this->line($message); + $this->count++; + $source->transaction_currency_id = (int)$sourceCurrency->id; $source->save(); } - if (null === $destAccountCurrency) { - Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $destAccount->id, $destAccount->name)); + if (null === $destCurrency) { + $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $destAccount->id, $destAccount->name); + Log::error($message); + $this->line($message); return; } // if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions: - if ((int)$destAccountCurrency->id === (int)$sourceAccountCurrency->id) { + if ((int)$destCurrency->id === (int)$sourceCurrency->id) { // update both transactions to match: $source->foreign_amount = null; $source->foreign_currency_id = null; @@ -343,22 +381,24 @@ class JournalCurrencies extends Command sprintf( 'Currency for account "%s" is %s, and currency for account "%s" is also %s, so %s #%d (#%d and #%d) has been verified to be to %s exclusively.', - $destAccount->name, $destAccountCurrency->code, - $sourceAccount->name, $sourceAccountCurrency->code, + $destAccount->name, $destCurrency->code, + $sourceAccount->name, $sourceCurrency->code, $journal->transactionType->type, $journal->id, - $source->id, $destination->id, $sourceAccountCurrency->code + $source->id, $destination->id, $sourceCurrency->code ) ); + $this->count++; return; } // if destination account currency is different, both transactions must have this currency as foreign currency id. - if (!((int)$destAccountCurrency->id === (int)$sourceAccountCurrency->id)) { - $source->foreign_currency_id = $destAccountCurrency->id; - $destination->foreign_currency_id = $destAccountCurrency->id; + if (!((int)$destCurrency->id === (int)$sourceCurrency->id)) { + $source->foreign_currency_id = $destCurrency->id; + $destination->foreign_currency_id = $destCurrency->id; $source->save(); $destination->save(); + $this->count++; Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $source->id, $destination->id)); } @@ -366,6 +406,7 @@ class JournalCurrencies extends Command if (null === $source->foreign_amount && null !== $destination->foreign_amount) { $source->foreign_amount = bcmul((string)$destination->foreign_amount, '-1'); $source->save(); + $this->count++; Log::debug(sprintf('Restored foreign amount of source transaction (1) #%d to %s', $source->id, $source->foreign_amount)); } @@ -373,6 +414,7 @@ class JournalCurrencies extends Command if (null === $destination->foreign_amount && null !== $destination->foreign_amount) { $destination->foreign_amount = bcmul((string)$destination->foreign_amount, '-1'); $destination->save(); + $this->count++; Log::debug(sprintf('Restored foreign amount of destination transaction (2) #%d to %s', $destination->id, $destination->foreign_amount)); } @@ -385,6 +427,7 @@ class JournalCurrencies extends Command $destination->foreign_amount = $destination->amount; $source->save(); $destination->save(); + $this->count++; return; } @@ -396,6 +439,7 @@ class JournalCurrencies extends Command $foreignAmount ) ); + $this->count++; $source->foreign_amount = bcmul($foreignPositive, '-1'); $destination->foreign_amount = $foreignPositive; $source->save(); @@ -415,12 +459,7 @@ class JournalCurrencies extends Command */ private function updateTransferCurrencies(): void { - $set = TransactionJournal - ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->where('transaction_types.type', TransactionType::TRANSFER) - ->with(['user', 'transactionType', 'transactionCurrency', 'transactions', 'transactions.account']) - ->get(['transaction_journals.*']); - + $set = $this->journalRepos->getAllJournals([TransactionType::TRANSFER]); /** @var TransactionJournal $journal */ foreach ($set as $journal) { $this->updateTransferCurrency($journal); @@ -435,6 +474,7 @@ class JournalCurrencies extends Command $sourceTransaction = $this->getSourceTransaction($transfer); $destTransaction = $this->getDestinationTransaction($transfer); + // @codeCoverageIgnoreStart if (null === $sourceTransaction) { $this->info(sprintf('Source transaction for journal #%d is null.', $transfer->id)); @@ -445,6 +485,7 @@ class JournalCurrencies extends Command return; } + // @codeCoverageIgnoreEnd $this->updateTransactionCurrency($transfer, $sourceTransaction, $destTransaction); $this->updateJournalCurrency($transfer, $sourceTransaction); diff --git a/app/Http/Controllers/System/InstallController.php b/app/Http/Controllers/System/InstallController.php index cc4ec6efd5..b070069ce9 100644 --- a/app/Http/Controllers/System/InstallController.php +++ b/app/Http/Controllers/System/InstallController.php @@ -67,7 +67,7 @@ class InstallController extends Controller $this->upgradeCommands = [ // there are x initial commands 'migrate' => ['--seed' => true, '--force' => true], - 'firefly:decrypt-all' => [], + 'firefly-iii:decrypt-all' => [], 'generate-keys' => [], // an exception :( // there are 10 upgrade commands. diff --git a/app/Models/Account.php b/app/Models/Account.php index b9c7fffb1d..a1ceafd595 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -165,24 +165,6 @@ class Account extends Model return $name; } - /** - * Returns the opening balance. - * - * @return TransactionJournal - */ - public function getOpeningBalance(): TransactionJournal - { - $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $this->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) - ->first(['transaction_journals.*']); - if (null === $journal) { - return new TransactionJournal; - } - - return $journal; - } - /** * @codeCoverageIgnore * Get all of the notes. diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 9d9e39b59c..88097add6b 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -38,10 +38,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property string $amount * @property Carbon created_at * @property Carbon updated_at - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at - * @property \Illuminate\Support\Carbon $date - * @property-read \FireflyIII\Models\TransactionJournal|null $transactionJournal * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\PiggyBankEvent newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\PiggyBankEvent newQuery() * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\PiggyBankEvent query() diff --git a/app/Providers/ImportServiceProvider.php b/app/Providers/ImportServiceProvider.php new file mode 100644 index 0000000000..81154dbb71 --- /dev/null +++ b/app/Providers/ImportServiceProvider.php @@ -0,0 +1,61 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Providers; + +use FireflyIII\Repositories\ImportJob\ImportJobRepository; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Illuminate\Foundation\Application; +use Illuminate\Support\ServiceProvider; + +/** + * @codeCoverageIgnore + * Class ImportServiceProvider. + */ +class ImportServiceProvider extends ServiceProvider +{ + /** + * Bootstrap the application services. + */ + public function boot(): void + { + } + + /** + * Register the application services. + */ + public function register(): void + { + $this->app->bind( + ImportJobRepositoryInterface::class, + function (Application $app) { + /** @var ImportJobRepositoryInterface $repository */ + $repository = app(ImportJobRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); + } +} diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 1acc585290..485afa99fe 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -70,7 +70,7 @@ class AccountRepository implements AccountRepositoryInterface /** * Moved here from account CRUD. * - * @param Account $account + * @param Account $account * @param Account|null $moveTo * * @return bool @@ -88,7 +88,7 @@ class AccountRepository implements AccountRepositoryInterface /** * @param string $number - * @param array $types + * @param array $types * * @return Account|null */ @@ -115,7 +115,7 @@ class AccountRepository implements AccountRepositoryInterface /** * @param string $iban - * @param array $types + * @param array $types * * @return Account|null */ @@ -141,7 +141,7 @@ class AccountRepository implements AccountRepositoryInterface /** * @param string $name - * @param array $types + * @param array $types * * @return Account|null */ @@ -229,9 +229,10 @@ class AccountRepository implements AccountRepositoryInterface if (count($accountIds) > 0) { $query->whereIn('accounts.id', $accountIds); } - $query->orderBy('accounts.name','ASC'); + $query->orderBy('accounts.name', 'ASC'); $result = $query->get(['accounts.*']); + return $result; } @@ -247,7 +248,7 @@ class AccountRepository implements AccountRepositoryInterface if (count($types) > 0) { $query->accountTypeIn($types); } - $query->orderBy('accounts.name','ASC'); + $query->orderBy('accounts.name', 'ASC'); $result = $query->get(['accounts.*']); @@ -271,8 +272,8 @@ class AccountRepository implements AccountRepositoryInterface $query->accountTypeIn($types); } $query->where('active', 1); - $query->orderBy('accounts.account_type_id','ASC'); - $query->orderBy('accounts.name','ASC'); + $query->orderBy('accounts.account_type_id', 'ASC'); + $query->orderBy('accounts.name', 'ASC'); $result = $query->get(['accounts.*']); return $result; @@ -322,7 +323,7 @@ class AccountRepository implements AccountRepositoryInterface * Return meta value for account. Null if not found. * * @param Account $account - * @param string $field + * @param string $field * * @return null|string */ @@ -546,7 +547,7 @@ class AccountRepository implements AccountRepositoryInterface /** * @param string $query - * @param array $types + * @param array $types * * @return Collection */ @@ -590,7 +591,7 @@ class AccountRepository implements AccountRepositoryInterface /** * @param Account $account - * @param array $data + * @param array $data * * @return Account * @throws \FireflyIII\Exceptions\FireflyException @@ -605,4 +606,17 @@ class AccountRepository implements AccountRepositoryInterface return $account; } + + /** + * @param Account $account + * @return TransactionJournal|null + */ + public function getOpeningBalance(Account $account): ?TransactionJournal + { + return TransactionJournal + ::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->first(['transaction_journals.*']); + } } diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 7d0382ca10..de8ddf7125 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -37,6 +37,12 @@ use Illuminate\Support\Collection; interface AccountRepositoryInterface { + /** + * @param Account $account + * @return TransactionJournal|null + */ + public function getOpeningBalance(Account $account): ?TransactionJournal; + /** * Moved here from account CRUD. * @@ -49,7 +55,7 @@ interface AccountRepositoryInterface /** * Moved here from account CRUD. * - * @param Account $account + * @param Account $account * @param Account|null $moveTo * * @return bool @@ -60,7 +66,7 @@ interface AccountRepositoryInterface * Find by account number. Is used. * * @param string $number - * @param array $types + * @param array $types * * @return Account|null */ @@ -68,7 +74,7 @@ interface AccountRepositoryInterface /** * @param string $iban - * @param array $types + * @param array $types * * @return Account|null */ @@ -76,7 +82,7 @@ interface AccountRepositoryInterface /** * @param string $name - * @param array $types + * @param array $types * * @return Account|null */ @@ -149,7 +155,7 @@ interface AccountRepositoryInterface * Return meta value for account. Null if not found. * * @param Account $account - * @param string $field + * @param string $field * * @return null|string */ @@ -250,7 +256,7 @@ interface AccountRepositoryInterface /** * @param string $query - * @param array $types + * @param array $types * * @return Collection */ @@ -270,7 +276,7 @@ interface AccountRepositoryInterface /** * @param Account $account - * @param array $data + * @param array $data * * @return Account */ diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 774c54438d..8603668ee9 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -791,4 +791,19 @@ class JournalRepository implements JournalRepositoryInterface return $journal; } + + /** + * Get all transaction journals with a specific type, regardless of user. + * + * @param array $types + * @return Collection + */ + public function getAllJournals(array $types): Collection + { + return TransactionJournal + ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->whereIn('transaction_types.type', $types) + ->with(['user', 'transactionType', 'transactionCurrency', 'transactions', 'transactions.account']) + ->get(['transaction_journals.*']); + } } diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 6d4825fd2b..44ab15720f 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Journal; use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; @@ -41,11 +40,20 @@ use Illuminate\Support\MessageBag; */ interface JournalRepositoryInterface { + + /** + * Get all transaction journals with a specific type, regardless of user. + * + * @param array $types + * @return Collection + */ + public function getAllJournals(array $types): Collection; + /** * @param TransactionJournal $journal - * @param TransactionType $type - * @param Account $source - * @param Account $destination + * @param TransactionType $type + * @param Account $source + * @param Account $destination * * @return MessageBag */ @@ -157,7 +165,7 @@ interface JournalRepositoryInterface * otherwise look for meta field and return that one. * * @param TransactionJournal $journal - * @param null|string $field + * @param null|string $field * * @return string */ @@ -208,7 +216,7 @@ interface JournalRepositoryInterface * Return Carbon value of a meta field (or NULL). * * @param TransactionJournal $journal - * @param string $field + * @param string $field * * @return null|Carbon */ @@ -218,7 +226,7 @@ interface JournalRepositoryInterface * Return string value of a meta date (or NULL). * * @param TransactionJournal $journal - * @param string $field + * @param string $field * * @return null|string */ @@ -228,7 +236,7 @@ interface JournalRepositoryInterface * Return value of a meta field (or NULL). * * @param TransactionJournal $journal - * @param string $field + * @param string $field * * @return null|string */ @@ -301,7 +309,7 @@ interface JournalRepositoryInterface /** * @param TransactionJournal $journal - * @param int $order + * @param int $order * * @return bool */ @@ -316,7 +324,7 @@ interface JournalRepositoryInterface * Update budget for a journal. * * @param TransactionJournal $journal - * @param int $budgetId + * @param int $budgetId * * @return TransactionJournal */ @@ -326,7 +334,7 @@ interface JournalRepositoryInterface * Update category for a journal. * * @param TransactionJournal $journal - * @param string $category + * @param string $category * * @return TransactionJournal */ @@ -336,7 +344,7 @@ interface JournalRepositoryInterface * Update tag(s) for a journal. * * @param TransactionJournal $journal - * @param array $tags + * @param array $tags * * @return TransactionJournal */ diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 27824c5928..fe40e893f8 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -391,10 +391,10 @@ class FireflyValidator extends Validator public function validateUniqueAccountForUser($attribute, $value, $parameters): bool { // because a user does not have to be logged in (tests and what-not). + if (!auth()->check()) { return $this->validateAccountAnonymously(); } - if (isset($this->data['what'])) { return $this->validateByAccountTypeString($value, $parameters, $this->data['what']); } @@ -409,7 +409,8 @@ class FireflyValidator extends Validator return $this->validateByAccountId($value); } - return false; + // without type, just try to validate the name. + return $this->validateByAccountName($value); } /** @@ -588,6 +589,7 @@ class FireflyValidator extends Validator $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { + // TODO no longer need to loop like this. if ($entry->name === $value) { return false; } @@ -598,7 +600,7 @@ class FireflyValidator extends Validator /** * @param string $value - * @param array $parameters + * @param array $parameters * @param string $type * * @return bool @@ -627,4 +629,13 @@ class FireflyValidator extends Validator return true; } + + /** + * @param string $value + * @return bool + */ + private function validateByAccountName(string $value): bool + { + return auth()->user()->accounts()->where('name', $value)->count() === 0; + } } diff --git a/config/app.php b/config/app.php index b0215adb1f..f95262a3b1 100644 --- a/config/app.php +++ b/config/app.php @@ -19,7 +19,9 @@ * along with Firefly III. If not, see . */ + declare(strict_types=1); +use FireflyIII\Providers\ImportServiceProvider; return [ @@ -96,6 +98,7 @@ return [ FireflyIII\Providers\TagServiceProvider::class, FireflyIII\Providers\AdminServiceProvider::class, FireflyIII\Providers\RecurringServiceProvider::class, + ImportServiceProvider::class, ], diff --git a/database/seeds/LinkTypeSeeder.php b/database/seeds/LinkTypeSeeder.php index 47fecbbd7c..01a23cdfe4 100644 --- a/database/seeds/LinkTypeSeeder.php +++ b/database/seeds/LinkTypeSeeder.php @@ -46,10 +46,11 @@ class LinkTypeSeeder extends Seeder 'outward' => '(partially) refunds', 'editable' => false, ], - ['name' => 'Paid', - 'inward' => 'is (partially) paid for by', - 'outward' => '(partially) pays for', - 'editable' => false, + [ + 'name' => 'Paid', + 'inward' => 'is (partially) paid for by', + 'outward' => '(partially) pays for', + 'editable' => false, ], [ 'name' => 'Reimbursement', diff --git a/phpunit.coverage.specific.xml b/phpunit.coverage.specific.xml index da7d5b821d..117e800802 100644 --- a/phpunit.coverage.specific.xml +++ b/phpunit.coverage.specific.xml @@ -32,16 +32,24 @@ - - ./tests/Feature - - - - ./tests/Unit - + ./tests/Api + + + ./tests/Unit/Console + + + + diff --git a/phpunit.coverage.xml b/phpunit.coverage.xml index 60bc506b70..e5cbcc7df2 100644 --- a/phpunit.coverage.xml +++ b/phpunit.coverage.xml @@ -32,16 +32,24 @@ - - ./tests/Feature - - - - ./tests/Unit - + ./tests/Api + + + ./tests/Unit/Console + + + + diff --git a/phpunit.xml b/phpunit.xml index 736d18a486..697a9238d2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -32,16 +32,24 @@ - - ./tests/Feature - - - - ./tests/Unit - + ./tests/Api + + + ./tests/Unit/Console + + + + diff --git a/tests/TestCase.php b/tests/TestCase.php index b0ecd8cecd..68af6f98cf 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -333,6 +333,9 @@ abstract class TestCase extends BaseTestCase DB::raw('COUNT(transaction_journal_id) as ct'), ] )->first(); + if(null === $result) { + throw new FireflyException(sprintf('Cannot find suitable %s to use.', $type)); + } return TransactionJournal::find((int)$result->transaction_journal_id); } diff --git a/tests/Unit/Console/Commands/Correction/CreateAccessTokensTest.php b/tests/Unit/Console/Commands/Correction/CreateAccessTokensTest.php new file mode 100644 index 0000000000..384b22c175 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/CreateAccessTokensTest.php @@ -0,0 +1,96 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\Preference; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Mockery; +use Preferences; +use Tests\TestCase; + +/** + * Class CreateAccessTokensTest + */ +class CreateAccessTokensTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\CreateAccessTokens + */ + public function testHandle(): void + { + $users = new Collection([$this->user()]); + $repository = $this->mock(UserRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('all')->atLeast()->once()->andReturn($users); + + // mock preferences thing: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'access_token', null]) + ->once()->andReturn(null); + + // null means user object will generate one and store it. + Preferences::shouldReceive('setForUser')->withArgs([Mockery::any(), 'access_token', Mockery::any()]) + ->once(); + + + $this->artisan('firefly-iii:create-access-tokens') + ->expectsOutput(sprintf('Generated access token for user %s', $this->user()->email)) + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\CreateAccessTokens + */ + public function testHandlePrefExists(): void + { + $users = new Collection([$this->user()]); + $repository = $this->mock(UserRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('all')->atLeast()->once()->andReturn($users); + + // mock preferences thing: + $preference = new Preference; + $preference->data = '123'; + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'access_token', null]) + ->once()->andReturn($preference); + + // null means user object will generate one and store it. + Preferences::shouldNotReceive('setForUser'); + + $this->artisan('firefly-iii:create-access-tokens') + ->expectsOutput('All access tokens OK!') + ->assertExitCode(0); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/CreateLinkTypesTest.php b/tests/Unit/Console/Commands/Correction/CreateLinkTypesTest.php new file mode 100644 index 0000000000..db04b21796 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/CreateLinkTypesTest.php @@ -0,0 +1,78 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\LinkType; +use Log; +use Tests\TestCase; + +/** + * Class CreateLinkTypesTest + */ +class CreateLinkTypesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\CreateLinkTypes + */ + public function testHandle(): void + { + // delete all other link types: + LinkType::whereNotIn('name', ['Related', 'Refund', 'Paid', 'Reimbursement'])->forceDelete(); + + // delete link type: + LinkType::where('name', 'Reimbursement')->forceDelete(); + $this->assertCount(3, LinkType::get()); + + // run command, expect output: + $this->artisan('firefly-iii:create-link-types') + ->expectsOutput('Created missing link type "Reimbursement"') + ->assertExitCode(0); + + $this->assertCount(4, LinkType::get()); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\CreateLinkTypes + */ + public function testHandleNothing(): void + { + $this->assertCount(4, LinkType::get()); + + // run command, expect output: + $this->artisan('firefly-iii:create-link-types') + ->expectsOutput('All link types OK!') + ->assertExitCode(0); + + $this->assertCount(4, LinkType::get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/DeleteEmptyGroupsTest.php b/tests/Unit/Console/Commands/Correction/DeleteEmptyGroupsTest.php new file mode 100644 index 0000000000..4d07dc7078 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/DeleteEmptyGroupsTest.php @@ -0,0 +1,70 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\TransactionGroup; +use Log; +use Tests\TestCase; + +/** + * Class DeleteEmptyGroupsTest + */ +class DeleteEmptyGroupsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteEmptyGroups + */ + public function testHandle(): void + { + // assume there are no empty groups.. + $this->artisan('firefly-iii:delete-empty-groups') + ->expectsOutput('No empty transaction groups.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteEmptyGroups + */ + public function testHandleWithGroup(): void + { + // create new group: + $group = TransactionGroup::create(['user_id' => 1]); + + // command should delete it. + $this->artisan('firefly-iii:delete-empty-groups') + ->expectsOutput('Deleted 1 empty transaction group(s).') + ->assertExitCode(0); + + // should not be able to find it: + $this->assertCount(0, TransactionGroup::where('id', $group->id)->whereNull('deleted_at')->get()); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/DeleteEmptyJournalsTest.php b/tests/Unit/Console/Commands/Correction/DeleteEmptyJournalsTest.php new file mode 100644 index 0000000000..85385a268e --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/DeleteEmptyJournalsTest.php @@ -0,0 +1,116 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Log; +use Tests\TestCase; + +class DeleteEmptyJournalsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteEmptyJournals + */ + public function testHandle(): void + { + // assume there are no empty journals or uneven journals + $this->artisan('firefly-iii:delete-empty-journals') + ->expectsOutput('No uneven transaction journals.') + ->expectsOutput('No empty transaction journals.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteEmptyJournals + */ + public function testHandleEmptyJournals(): void + { + // create empty journal: + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => 1, + 'description' => 'Hello', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $this->artisan('firefly-iii:delete-empty-journals') + ->expectsOutput('No uneven transaction journals.') + ->expectsOutput(sprintf('Deleted empty transaction journal #%d', $journal->id)) + ->assertExitCode(0); + + // verify its indeed gone + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNull('deleted_at')->get()); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteEmptyJournals + */ + public function testHandleUnevenJournals(): void + { + // create empty journal: + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => 1, + 'description' => 'Hello', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + + // link empty transaction + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => 1, + 'amount' => '5', + ] + ); + + + $this->artisan('firefly-iii:delete-empty-journals') + ->expectsOutput(sprintf('Deleted transaction journal #%d because it had an uneven number of transactions.', $journal->id)) + ->expectsOutput('No empty transaction journals.') + ->assertExitCode(0); + + // verify both are gone + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNull('deleted_at')->get()); + $this->assertCount(0, Transaction::where('id', $transaction->id)->whereNull('deleted_at')->get()); + } + + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/DeleteOrphanedTransactionsTest.php b/tests/Unit/Console/Commands/Correction/DeleteOrphanedTransactionsTest.php new file mode 100644 index 0000000000..d9ce06b711 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/DeleteOrphanedTransactionsTest.php @@ -0,0 +1,144 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\Account; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Log; +use Tests\TestCase; + +/** + * Class DeleteOrphanedTransactionsTest + */ +class DeleteOrphanedTransactionsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteOrphanedTransactions + */ + public function testHandle(): void + { + // assume there are no orphaned transactions. + $this->artisan('firefly-iii:delete-orphaned-transactions') + ->expectsOutput('No orphaned transactions.') + ->expectsOutput('No orphaned accounts.') + ->assertExitCode(0); + } + + /** + * + */ + public function testHandleOrphanedAccounts(): void + { + + // create deleted account: + $account = Account::create( + [ + 'user_id' => 1, + 'name' => 'Some account', + 'account_type_id' => 1, + + ] + ); + $account->delete(); + + // create NOT deleted journal + transaction. + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => 1, + 'description' => 'Hello', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $account->id, + 'amount' => '5', + ] + ); + + $this->artisan('firefly-iii:delete-orphaned-transactions') + ->expectsOutput('No orphaned transactions.') + ->expectsOutput(sprintf('Deleted transaction journal #%d because account #%d was already deleted.', + $journal->id, $account->id)) + ->assertExitCode(0); + + // verify bad objects are gone. + $this->assertCount(0, Transaction::where('id', $transaction->id)->whereNull('deleted_at')->get()); + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNull('deleted_at')->get()); + $this->assertCount(0, Account::where('id', $account->id)->whereNull('deleted_at')->get()); + + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteOrphanedTransactions + */ + public function testHandleOrphanedTransactions(): void + { + // create deleted journal: + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => 1, + 'description' => 'Hello', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $journal->delete(); + + // create NOT deleted transaction. + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => 1, + 'amount' => '5', + ] + ); + + $this->artisan('firefly-iii:delete-orphaned-transactions') + ->expectsOutput(sprintf('Transaction #%d (part of deleted transaction journal #%d) has been deleted as well.', + $transaction->id, $journal->id)) + ->expectsOutput('No orphaned accounts.') + ->assertExitCode(0); + + // verify objects are gone. + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNull('deleted_at')->get()); + $this->assertCount(0, Transaction::where('id', $transaction->id)->whereNull('deleted_at')->get()); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/DeleteZeroAmountTest.php b/tests/Unit/Console/Commands/Correction/DeleteZeroAmountTest.php new file mode 100644 index 0000000000..946a03672e --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/DeleteZeroAmountTest.php @@ -0,0 +1,91 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Log; +use Tests\TestCase; + +/** + * Class DeleteZeroAmountTest + */ +class DeleteZeroAmountTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteZeroAmount + */ + public function testHandle(): void + { + // assume there are no transactions with a zero amount. + $this->artisan('firefly-iii:delete-zero-amount') + ->expectsOutput('No zero-amount transaction journals.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\DeleteZeroAmount + */ + public function testHandleTransactions(): void + { + $account = $this->getRandomAsset(); + // create NOT deleted journal + transaction. + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => 1, + 'description' => 'Hello', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $account->id, + 'amount' => '0', + ] + ); + + + // assume there are no transactions with a zero amount. + $this->artisan('firefly-iii:delete-zero-amount') + ->expectsOutput(sprintf('Deleted transaction journal #%d because the amount is zero (0.00).', $journal->id)) + ->assertExitCode(0); + + // verify objects are gone. + $this->assertCount(0, Transaction::where('id', $transaction->id)->whereNull('deleted_at')->get()); + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNull('deleted_at')->get()); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/EnableCurrenciesTest.php b/tests/Unit/Console/Commands/Correction/EnableCurrenciesTest.php new file mode 100644 index 0000000000..6c29cb5b73 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/EnableCurrenciesTest.php @@ -0,0 +1,82 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionCurrency; +use Log; +use Tests\TestCase; + +/** + * Class EnableCurrenciesTest + */ +class EnableCurrenciesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\EnableCurrencies + */ + public function testHandle(): void + { + // assume the current database is intact. + $count = TransactionCurrency::where('enabled', 1)->count(); + + $this->artisan('firefly-iii:enable-currencies') + ->expectsOutput('All currencies are correctly enabled or disabled.') + ->assertExitCode(0); + + + $this->assertCount($count, TransactionCurrency::where('enabled', 1)->get()); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\EnableCurrencies + */ + public function testHandleDisabled(): void + { + // find a disabled currency, update a budget limit with it. + $currency = TransactionCurrency::where('enabled', 0)->first(); + /** @var BudgetLimit $budgetLimit */ + $budgetLimit = BudgetLimit::inRandomOrder()->first(); + $budgetLimit->transaction_currency_id = $currency->id; + $budgetLimit->save(); + + // assume the current database is intact. + $count = TransactionCurrency::where('enabled', 1)->count(); + $this->artisan('firefly-iii:enable-currencies') + ->expectsOutput(sprintf('%d were (was) still disabled. This has been corrected.', 1)) + ->assertExitCode(0); + + // assume its been enabled. + $this->assertCount($count + 1, TransactionCurrency::where('enabled', 1)->get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/FixAccountTypesTest.php b/tests/Unit/Console/Commands/Correction/FixAccountTypesTest.php new file mode 100644 index 0000000000..7d6b9d0cea --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/FixAccountTypesTest.php @@ -0,0 +1,371 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Log; +use Tests\TestCase; + +/** + * Class FixAccountTypesTest + */ +class FixAccountTypesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\FixAccountTypes + */ + public function testHandleUneven(): void + { + $this->mock(AccountFactory::class); + $source = $this->user()->accounts()->where('name', 'Another DUO Student loans')->first(); + $type = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $type->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + + // assume there's nothing to fix. + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput(sprintf('Cannot inspect transaction journal #%d because it has 1 transaction(s) instead of 2.', $journal->id)) + ->assertExitCode(0); + $one->forceDelete(); + $journal->forceDelete(); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\FixAccountTypes + */ + public function testHandle(): void + { + $this->mock(AccountFactory::class); + + // assume there's nothing to fix. + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput('All account types are OK!') + ->assertExitCode(0); + } + + /** + * Try to fix a withdrawal that goes from a loan to another loan. + * + * @covers \FireflyIII\Console\Commands\Correction\FixAccountTypes + */ + public function testHandleWithdrawalLoanLoan(): void + { + $this->mock(AccountFactory::class); + $source = $this->user()->accounts()->where('name', 'Another DUO Student loans')->first(); + $destination = $this->user()->accounts()->where('name', 'DUO Student loans')->first(); + $type = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $type->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $destination->id, + 'amount' => '10', + ] + ); + + + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput(sprintf('The source account of %s #%d cannot be of type "%s".', $type->type, $journal->id, 'Loan')) + ->expectsOutput(sprintf('The destination account of %s #%d cannot be of type "%s".', $type->type, $journal->id, 'Loan')) + ->expectsOutput('Acted on 1 transaction(s)!') + ->assertExitCode(0); + + // since system cant handle this problem, dont look for changed transactions. + + + $one->forceDelete(); + $two->forceDelete(); + $journal->forceDelete(); + } + + /** + * Transferring from an asset to a loan should be a withdrawal, not a transfer + */ + public function testHandleTransferAssetLoan(): void + { + $this->mock(AccountFactory::class); + $source = $this->getRandomAsset(); + $destination = $this->user()->accounts()->where('name', 'DUO Student loans')->first(); + $type = TransactionType::where('type', TransactionType::TRANSFER)->first(); + $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $type->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $destination->id, + 'amount' => '10', + ] + ); + + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput(sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id)) + ->expectsOutput('Acted on 1 transaction(s)!') + ->assertExitCode(0); + + // verify the change has been made. + $this->assertCount(1, TransactionJournal::where('id', $journal->id)->where('transaction_type_id', $withdrawal->id)->get()); + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->where('transaction_type_id', $type->id)->get()); + + $one->forceDelete(); + $two->forceDelete(); + $journal->forceDelete(); + } + + /** + * Transferring from a loan to an asset should be a deposit, not a transfer + */ + public function testHandleTransferLoanAsset(): void + { + $this->mock(AccountFactory::class); + $source = $this->user()->accounts()->where('name', 'DUO Student loans')->first(); + $destination = $this->getRandomAsset(); + $type = TransactionType::where('type', TransactionType::TRANSFER)->first(); + $deposit = TransactionType::where('type', TransactionType::DEPOSIT)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $type->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $destination->id, + 'amount' => '10', + ] + ); + + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput(sprintf('Converted transaction #%d from a transfer to a deposit.', $journal->id)) + ->expectsOutput('Acted on 1 transaction(s)!') + ->assertExitCode(0); + + // verify the change has been made. + $this->assertCount(1, TransactionJournal::where('id', $journal->id)->where('transaction_type_id', $deposit->id)->get()); + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->where('transaction_type_id', $type->id)->get()); + + $one->forceDelete(); + $two->forceDelete(); + $journal->forceDelete(); + } + + /** + * Withdrawal with a revenue account as a destination must be converted. + */ + public function testHandleWithdrawalAssetRevenue(): void + { + $source = $this->getRandomAsset(); + $destination = $this->getRandomRevenue(); + $newDestination = $this->getRandomExpense(); + $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $withdrawal->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $destination->id, + 'amount' => '10', + ] + ); + + $this->assertCount(0, Transaction::where('id', $two->id)->where('account_id', $newDestination->id)->get()); + $this->assertCount(1, Transaction::where('id', $two->id)->where('account_id', $destination->id)->get()); + + // mock stuff + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->atLeast()->once(); + $factory->shouldReceive('findOrCreate') + ->withArgs([$destination->name, AccountType::EXPENSE]) + ->atLeast()->once()->andReturn($newDestination); + + // Transaction journal #137, destination account changed from #1 ("Checking Account") to #29 ("Land lord"). + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput( + sprintf('Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', + $journal->id, + $destination->id, $destination->name, + $newDestination->id, $newDestination->name + )) + ->expectsOutput('Acted on 1 transaction(s)!') + ->assertExitCode(0); + + // verify the change has been made + $this->assertCount(1, Transaction::where('id', $two->id)->where('account_id', $newDestination->id)->get()); + $this->assertCount(0, Transaction::where('id', $two->id)->where('account_id', $destination->id)->get()); + + $one->forceDelete(); + $two->forceDelete(); + $journal->forceDelete(); + } + + /** + * Deposit with an expense account as a source instead of a revenue account must be converted. + */ + public function testHandleDepositAssetExpense(): void + { + $source = $this->getRandomExpense(); + $newSource = $this->getRandomRevenue(); + $destination = $this->getRandomAsset(); + + $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $deposit = TransactionType::where('type', TransactionType::DEPOSIT)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $deposit->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $source->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $destination->id, + 'amount' => '10', + ] + ); + + $this->assertCount(0, Transaction::where('id', $one->id)->where('account_id', $newSource->id)->get()); + $this->assertCount(1, Transaction::where('id', $one->id)->where('account_id', $source->id)->get()); + + // mock stuff + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->atLeast()->once(); + $factory->shouldReceive('findOrCreate') + ->withArgs([$source->name, AccountType::REVENUE]) + ->atLeast()->once()->andReturn($newSource); + + // Transaction journal #137, destination account changed from #1 ("Checking Account") to #29 ("Land lord"). + $this->artisan('firefly-iii:fix-account-types') + ->expectsOutput( + sprintf('Transaction journal #%d, source account changed from #%d ("%s") to #%d ("%s").', + $journal->id, + $destination->id, $destination->name, + $newSource->id, $newSource->name + )) + ->expectsOutput('Acted on 1 transaction(s)!') + ->assertExitCode(0); + + $this->assertCount(1, Transaction::where('id', $one->id)->where('account_id', $newSource->id)->get()); + $this->assertCount(0, Transaction::where('id', $one->id)->where('account_id', $source->id)->get()); + + $one->forceDelete(); + $two->forceDelete(); + $journal->forceDelete(); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/FixPiggiesTest.php b/tests/Unit/Console/Commands/Correction/FixPiggiesTest.php new file mode 100644 index 0000000000..6f71a03ab6 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/FixPiggiesTest.php @@ -0,0 +1,130 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankEvent; +use Log; +use Tests\TestCase; + +/** + * Class FixPiggiesTest + */ +class FixPiggiesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Null event. + * + * @covers \FireflyIII\Console\Commands\Correction\FixPiggies + */ + public function testHandleNull(): void + { + /** @var PiggyBank $piggy */ + $piggy = $this->user()->piggyBanks()->inRandomOrder()->first(); + + // create event to trigger console commands. + $event = PiggyBankEvent::create( + [ + 'piggy_bank_id' => $piggy->id, + 'date' => '2019-01-01', + 'amount' => 5, + ] + ); + + // assume there's nothing to fix. + $this->artisan('firefly-iii:fix-piggies') + ->expectsOutput('All piggy bank events are correct.') + ->assertExitCode(0); + $event->forceDelete(); + } + + /** + * Withdrawal instead of transfer + * + * @covers \FireflyIII\Console\Commands\Correction\FixPiggies + */ + public function testHandleBadJournal(): void + { + /** @var PiggyBank $piggy */ + $piggy = $this->user()->piggyBanks()->inRandomOrder()->first(); + $withdrawal = $this->getRandomWithdrawal(); + // create event to trigger console commands. + $event = PiggyBankEvent::create( + [ + 'piggy_bank_id' => $piggy->id, + 'date' => '2019-01-01', + 'amount' => 5, + 'transaction_journal_id' => $withdrawal->id, + ] + ); + + // assume there's nothing to fix. + $this->artisan('firefly-iii:fix-piggies') + ->expectsOutput(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $piggy->id)) + ->expectsOutput('Fixed 1 piggy bank event(s).') + ->assertExitCode(0); + + // verify update + $this->assertCount(0, PiggyBankEvent::where('id', $event->id)->where('transaction_journal_id', $withdrawal->id)->get()); + } + + /** + * Withdrawal instead of transfer + * + * @covers \FireflyIII\Console\Commands\Correction\FixPiggies + */ + public function testHandleDeletedJournal(): void + { + /** @var PiggyBank $piggy */ + $piggy = $this->user()->piggyBanks()->inRandomOrder()->first(); + $transfer = $this->getRandomTransfer(); + $event = PiggyBankEvent::create( + [ + 'piggy_bank_id' => $piggy->id, + 'date' => '2019-01-01', + 'amount' => 5, + 'transaction_journal_id' => $transfer->id, + ] + ); + $transfer->deleted_at = '2019-01-01 12:00:00'; + $transfer->save(); + $transfer->refresh(); + + $this->artisan('firefly-iii:fix-piggies') + ->expectsOutput('Fixed 1 piggy bank event(s).') + ->assertExitCode(0); + + // verify update + $this->assertCount(0, PiggyBankEvent::where('id', $event->id)->where('transaction_journal_id', $transfer->id)->get()); + $event->forceDelete(); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/FixUnevenAmountTest.php b/tests/Unit/Console/Commands/Correction/FixUnevenAmountTest.php new file mode 100644 index 0000000000..d5aa891a7a --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/FixUnevenAmountTest.php @@ -0,0 +1,101 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Log; +use Tests\TestCase; + +/** + * Class FixUnevenAmountTest + */ +class FixUnevenAmountTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\FixUnevenAmount + */ + public function testHandle(): void + { + // assume there's nothing to fix. + $this->artisan('firefly-iii:fix-uneven-amount') + ->expectsOutput('Amount integrity OK!') + ->assertExitCode(0); + + // dont verify anything + } + + /** + * Create uneven journal + * @covers \FireflyIII\Console\Commands\Correction\FixUnevenAmount + */ + public function testHandleUneven(): void + { + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journal = TransactionJournal::create( + [ + 'user_id' => 1, + 'transaction_currency_id' => 1, + 'transaction_type_id' => $withdrawal->id, + 'description' => 'Test', + 'tag_count' => 0, + 'date' => '2019-01-01', + ] + ); + $one = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $asset->id, + 'amount' => '-10', + ] + ); + $two = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $expense->id, + 'amount' => '12', + ] + ); + + $this->artisan('firefly-iii:fix-uneven-amount') + ->expectsOutput(sprintf('Corrected amount in transaction journal #%d', $journal->id)) + ->assertExitCode(0); + + // verify change. + $this->assertCount(1, Transaction::where('id', $one->id)->where('amount', '-10')->get()); + $this->assertCount(1, Transaction::where('id', $two->id)->where('amount', '10')->get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/RemoveBillsTest.php b/tests/Unit/Console/Commands/Correction/RemoveBillsTest.php new file mode 100644 index 0000000000..ef2b5b35ac --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/RemoveBillsTest.php @@ -0,0 +1,75 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\TransactionJournal; +use Log; +use Tests\TestCase; + +/** + * Class RemoveBillsTest + */ +class RemoveBillsTest extends TestCase +{ + + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\RemoveBills + */ + public function testHandle(): void + { + // assume there's nothing to fix. + $this->artisan('firefly-iii:remove-bills') + ->expectsOutput('All transaction journals have correct bill information.') + ->assertExitCode(0); + + // dont verify anything + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\RemoveBills + */ + public function testHandleWithdrawal(): void + { + $bill = $this->user()->bills()->first(); + $journal = $this->getRandomDeposit(); + + $journal->bill_id = $bill->id; + $journal->save(); + + $this->artisan('firefly-iii:remove-bills') + ->expectsOutput(sprintf('Transaction journal #%d should not be linked to bill #%d.', $journal->id, $bill->id)) + ->assertExitCode(0); + + // verify change + $this->assertCount(0, TransactionJournal::where('id', $journal->id)->whereNotNull('bill_id')->get()); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/RenameMetaFieldsTest.php b/tests/Unit/Console/Commands/Correction/RenameMetaFieldsTest.php new file mode 100644 index 0000000000..95a4db4118 --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/RenameMetaFieldsTest.php @@ -0,0 +1,76 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use FireflyIII\Models\TransactionJournalMeta; +use Log; +use Tests\TestCase; + +/** + * Class RenameMetaFieldsTest + */ +class RenameMetaFieldsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\RenameMetaFields + */ + public function testHandle(): void + { + $this->artisan('firefly-iii:rename-meta-fields') + ->expectsOutput('All meta fields are correct.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\RenameMetaFields + */ + public function testHandleFixed(): void + { + $withdrawal = $this->getRandomWithdrawal(); + $entry = TransactionJournalMeta::create( + [ + 'transaction_journal_id' => $withdrawal->id, + 'name' => 'importHashV2', + 'data' => 'Fake data', + + ] + ); + + $this->artisan('firefly-iii:rename-meta-fields') + ->expectsOutput('Renamed 1 meta field(s).') + ->assertExitCode(0); + + // verify update + $this->assertCount(1, TransactionJournalMeta::where('id', $entry->id)->where('name', 'import_hash_v2')->get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Correction/TransferBudgetsTest.php b/tests/Unit/Console/Commands/Correction/TransferBudgetsTest.php new file mode 100644 index 0000000000..5512b2a86e --- /dev/null +++ b/tests/Unit/Console/Commands/Correction/TransferBudgetsTest.php @@ -0,0 +1,71 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Correction; + + +use Log; +use Tests\TestCase; + +/** + * Class TransferBudgetsTest + */ +class TransferBudgetsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\TransferBudgets + */ + public function testHandle(): void + { + $this->artisan('firefly-iii:fix-transfer-budgets') + ->expectsOutput('No invalid budget/journal entries.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Correction\TransferBudgets + */ + public function testHandleBudget(): void + { + $deposit = $this->getRandomDeposit(); + $budget = $this->user()->budgets()->inRandomOrder()->first(); + + $deposit->budgets()->save($budget); + + $this->artisan('firefly-iii:fix-transfer-budgets') + ->expectsOutput(sprintf('Transaction journal #%d is a %s, so has no longer a budget.', $deposit->id, $deposit->transactionType->type)) + ->expectsOutput('Corrected 1 invalid budget/journal entries (entry).') + ->assertExitCode(0); + + // verify change + $this->assertCount(0, $deposit->budgets()->get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/DecryptDatabaseTest.php b/tests/Unit/Console/Commands/DecryptDatabaseTest.php new file mode 100644 index 0000000000..35c65bff5a --- /dev/null +++ b/tests/Unit/Console/Commands/DecryptDatabaseTest.php @@ -0,0 +1,140 @@ +. + */ + +namespace Tests\Unit\Console\Commands; + + +use Crypt; +use FireflyConfig; +use FireflyIII\Models\Account; +use FireflyIII\Models\Configuration; +use Log; +use Mockery; +use Tests\TestCase; + +/** + * Class DecryptDatabaseTest + */ +class DecryptDatabaseTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\DecryptDatabase + */ + public function testHandle(): void + { + // create encrypted account: + $name = 'Encrypted name'; + $iban = 'HR1723600001101234565'; + $account = Account::create( + [ + 'user_id' => 1, + 'account_type_id' => 1, + 'name' => Crypt::encrypt($name), + 'iban' => Crypt::encrypt($iban), + ]); + + + + FireflyConfig::shouldReceive('get')->withArgs([Mockery::any(), false])->atLeast()->once()->andReturn(null); + FireflyConfig::shouldReceive('set')->withArgs([Mockery::any(), true])->atLeast()->once(); + + $this->artisan('firefly-iii:decrypt-all') + ->expectsOutput('Done!') + ->assertExitCode(0); + + $this->assertCount(1, Account::where('id', $account->id)->where('name', $name)->get()); + $this->assertCount(1, Account::where('id', $account->id)->where('iban', $iban)->get()); + } + + /** + * @covers \FireflyIII\Console\Commands\DecryptDatabase + */ + public function testHandleDecrypted(): void + { + // create encrypted account: + $name = 'Encrypted name'; + $iban = 'HR1723600001101234565'; + $encryptedName = Crypt::encrypt($name); + $encryptedIban = Crypt::encrypt($iban); + $account = Account::create( + [ + 'user_id' => 1, + 'account_type_id' => 1, + 'name' => $encryptedName, + 'iban' => $encryptedIban, + ]); + + // pretend its not yet decrypted. + $true = new Configuration; + $true->data = true; + + FireflyConfig::shouldReceive('get')->withArgs([Mockery::any(), false])->atLeast()->once()->andReturn($true); + + $this->artisan('firefly-iii:decrypt-all') + ->expectsOutput('Done!') + ->assertExitCode(0); + + $this->assertCount(1, Account::where('id', $account->id)->where('name', $encryptedName)->get()); + $this->assertCount(1, Account::where('id', $account->id)->where('iban', $encryptedIban)->get()); + } + + /** + * Try to decrypt data that isn't actually encrypted. + * + * @covers \FireflyIII\Console\Commands\DecryptDatabase + */ + public function testHandleNotEncrypted(): void + { + // create encrypted account: + $name = 'Encrypted name'; + $iban = 'HR1723600001101234565'; + $account = Account::create( + [ + 'user_id' => 1, + 'account_type_id' => 1, + 'name' => $name, + 'iban' => $iban, + ]); + + // pretend its not yet decrypted. + $true = new Configuration; + $true->data = true; + + FireflyConfig::shouldReceive('get')->withArgs([Mockery::any(), false])->atLeast()->once()->andReturn($true); + + $this->artisan('firefly-iii:decrypt-all') + ->expectsOutput('Done!') + ->assertExitCode(0); + + $this->assertCount(1, Account::where('id', $account->id)->where('name', $name)->get()); + $this->assertCount(1, Account::where('id', $account->id)->where('iban', $iban)->get()); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Import/CreateCSVImportTest.php b/tests/Unit/Console/Commands/Import/CreateCSVImportTest.php new file mode 100644 index 0000000000..94e31c68c3 --- /dev/null +++ b/tests/Unit/Console/Commands/Import/CreateCSVImportTest.php @@ -0,0 +1,433 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Import; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Import\Routine\FileRoutine; +use FireflyIII\Import\Storage\ImportArrayStorage; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\Preference; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Illuminate\Support\MessageBag; +use Log; +use Mockery; +use Preferences; +use Tests\TestCase; + +/** + * Class CreateCSVImportTest + */ +class CreateCSVImportTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Covers a default run with perfect arguments. + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandle(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $fileRoutine = $this->mock(FileRoutine::class); + $storage = $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + $importJob = $this->user()->importJobs()->first(); + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn(new MessageBag); + $jobRepos->shouldReceive('setConfiguration')->atLeast()->once(); + + // job is ready to run. + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'provider_finished'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storing_data'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storage_finished'])->atLeast()->once(); + + // file routine gets called. + $fileRoutine->shouldReceive('setImportJob')->atLeast()->once(); + $fileRoutine->shouldReceive('run')->atLeast()->once(); + + // store data thing gets called. + $storage->shouldReceive('setImportJob')->atLeast()->once(); + $storage->shouldReceive('store')->atLeast()->once(); + + // mock Preferences. + Preferences::shouldReceive('setForUser')->atLeast()->once()->withArgs([Mockery::any(), 'lastActivity', Mockery::any()]); + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->assertExitCode(0); + + // this method imports nothing so there is nothing to verify. + + } + + /** + * Covers a default run with perfect arguments, but no import tag + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandleNoTag(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $fileRoutine = $this->mock(FileRoutine::class); + $storage = $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + + $importJob = ImportJob::create( + [ + 'key' => 'key-' . random_int(1, 100000), + 'user_id' => 1, + 'file_type' => 'csv', + 'status' => 'new', + 'errors' => [], + ] + ); + + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn(new MessageBag); + $jobRepos->shouldReceive('setConfiguration')->atLeast()->once(); + + // job is ready to run. + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'provider_finished'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storing_data'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storage_finished'])->atLeast()->once(); + + // file routine gets called. + $fileRoutine->shouldReceive('setImportJob')->atLeast()->once(); + $fileRoutine->shouldReceive('run')->atLeast()->once(); + + // store data thing gets called. + $storage->shouldReceive('setImportJob')->atLeast()->once(); + $storage->shouldReceive('store')->atLeast()->once(); + + // mock Preferences. + Preferences::shouldReceive('setForUser')->atLeast()->once()->withArgs([Mockery::any(), 'lastActivity', Mockery::any()]); + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->expectsOutput('No transactions have been imported :(.') + ->assertExitCode(0); + + // this method imports nothing so there is nothing to verify. + } + + /** + * Covers a default run with perfect arguments, but errors after importing. + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandleErrors(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $fileRoutine = $this->mock(FileRoutine::class); + $storage = $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + + $importJob = ImportJob::create( + [ + 'key' => 'key-' . random_int(1, 100000), + 'user_id' => 1, + 'file_type' => 'csv', + 'status' => 'new', + 'errors' => ['I am an error'], + ] + ); + + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn(new MessageBag); + $jobRepos->shouldReceive('setConfiguration')->atLeast()->once(); + + // job is ready to run. + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'provider_finished'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storing_data'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storage_finished'])->atLeast()->once(); + + // file routine gets called. + $fileRoutine->shouldReceive('setImportJob')->atLeast()->once(); + $fileRoutine->shouldReceive('run')->atLeast()->once(); + + // store data thing gets called. + $storage->shouldReceive('setImportJob')->atLeast()->once(); + $storage->shouldReceive('store')->atLeast()->once(); + + // mock Preferences. + Preferences::shouldReceive('setForUser')->atLeast()->once()->withArgs([Mockery::any(), 'lastActivity', Mockery::any()]); + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->expectsOutput('- I am an error') + ->assertExitCode(0); + + // this method imports nothing so there is nothing to verify. + } + + /** + * Crash while storing data. + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandleCrashStorage(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $fileRoutine = $this->mock(FileRoutine::class); + $storage = $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + $importJob = $this->user()->importJobs()->first(); + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn(new MessageBag); + $jobRepos->shouldReceive('setConfiguration')->atLeast()->once(); + + // job is ready to run. + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'provider_finished'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'storing_data'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'error'])->atLeast()->once(); + + // file routine gets called. + $fileRoutine->shouldReceive('setImportJob')->atLeast()->once(); + $fileRoutine->shouldReceive('run')->atLeast()->once(); + + // store data thing gets called. + $storage->shouldReceive('setImportJob')->atLeast()->once(); + $storage->shouldReceive('store')->atLeast()->once()->andThrow(new FireflyException('I am storage error.')); + + // mock Preferences. + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->expectsOutput('The import routine crashed: I am storage error.') + ->assertExitCode(1); + + // this method imports nothing so there is nothing to verify. + + } + + /** + * The file processor crashes for some reason. + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandleCrashProcess(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $fileRoutine = $this->mock(FileRoutine::class); + $storage = $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + $importJob = $this->user()->importJobs()->first(); + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn(new MessageBag); + $jobRepos->shouldReceive('setConfiguration')->atLeast()->once(); + + // job is ready to run. + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->atLeast()->once(); + $jobRepos->shouldReceive('setStatus')->withArgs([Mockery::any(), 'error'])->atLeast()->once(); + + // file routine gets called. + $fileRoutine->shouldReceive('setImportJob')->atLeast()->once(); + $fileRoutine->shouldReceive('run')->atLeast()->once()->andThrows(new FireflyException('I am big bad exception.')); + + // mock Preferences. + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->expectsOutput('The import routine crashed: I am big bad exception.') + ->assertExitCode(1); + + // this method imports nothing so there is nothing to verify. + + } + + /** + * Throw error when storing data. + * + * @covers \FireflyIII\Console\Commands\Import\CreateCSVImport + */ + public function testHandleFileStoreError(): void + { + $userRepos = $this->mock(UserRepositoryInterface::class); + $jobRepos = $this->mock(ImportJobRepositoryInterface::class); + $this->mock(FileRoutine::class); + $this->mock(ImportArrayStorage::class); + $user = $this->user(); + $token = new Preference; + $importJob = $this->user()->importJobs()->first(); + $file = storage_path('build/test-upload.csv'); + $config = storage_path('build/configuration.json'); + $messages = new MessageBag; + $messages->add('file', 'Some file error.'); + + // set preferences: + $token->data = 'token'; + + // mock calls to repository: + $userRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($user); + $jobRepos->shouldReceive('setUser')->atLeast()->once(); + $jobRepos->shouldReceive('create')->atLeast()->once()->andReturn($importJob); + $jobRepos->shouldReceive('storeCLIupload')->atLeast()->once()->andReturn($messages); + + // mock Preferences. + Preferences::shouldReceive('getForUser')->atLeast()->once()->withArgs([Mockery::any(), 'access_token', null])->andReturn($token); + + + $parameters = [ + $file, + $config, + '--user=1', + '--token=token', + ]; + $this->artisan('firefly-iii:csv-import ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Import file : %s', $file)) + ->expectsOutput(sprintf('Configuration file : %s', $config)) + ->expectsOutput('User : #1 (thegrumpydictator@gmail.com)') + ->expectsOutput(sprintf('Job : %s', $importJob->key)) + ->expectsOutput('Some file error.') + ->assertExitCode(1); + + // this method imports nothing so there is nothing to verify. + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Integrity/ReportEmptyObjectsTest.php b/tests/Unit/Console/Commands/Integrity/ReportEmptyObjectsTest.php new file mode 100644 index 0000000000..48f6a8f9ba --- /dev/null +++ b/tests/Unit/Console/Commands/Integrity/ReportEmptyObjectsTest.php @@ -0,0 +1,148 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Integrity; + + +use FireflyIII\Models\Account; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\Tag; +use Log; +use Tests\TestCase; + +/** + * Class ReportEmptyObjectsTest + */ +class ReportEmptyObjectsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Run basic test routine. + * + * @covers \FireflyIII\Console\Commands\Integrity\ReportEmptyObjects + */ + public function testHandleBudget(): void + { + $user = $this->user(); + $budget = Budget::create( + [ + 'user_id' => $user->id, + 'name' => 'Some budget', + ]); + $budgetLine = sprintf('User #%d (%s) has budget #%d ("%s") which has no transaction journals.', + $user->id, $user->email, $budget->id, $budget->name); + $budgetLimitLine = sprintf('User #%d (%s) has budget #%d ("%s") which has no budget limits.', + $user->id, $user->email, $budget->id, $budget->name); + + $this->artisan('firefly-iii:report-empty-objects') + ->expectsOutput($budgetLine) + ->expectsOutput($budgetLimitLine) + ->assertExitCode(0); + $budget->forceDelete(); + + // this method changes no objects so there is nothing to verify. + } + + /** + * Run basic test routine. + * + * @covers \FireflyIII\Console\Commands\Integrity\ReportEmptyObjects + */ + public function testHandleCategory(): void + { + $user = $this->user(); + $category = Category::create( + [ + 'user_id' => $user->id, + 'name' => 'Some category', + ]); + $categoryLine = sprintf('User #%d (%s) has category #%d ("%s") which has no transaction journals.', + $user->id, $user->email, $category->id, $category->name); + + $this->artisan('firefly-iii:report-empty-objects') + ->expectsOutput($categoryLine) + ->assertExitCode(0); + $category->forceDelete(); + + // this method changes no objects so there is nothing to verify. + } + + /** + * Run basic test routine. + * + * @covers \FireflyIII\Console\Commands\Integrity\ReportEmptyObjects + */ + public function testHandleTag(): void + { + $user = $this->user(); + $tag = Tag::create( + [ + 'user_id' => $user->id, + 'tag' => 'Some tag', + 'tagMode' => 'nothing', + ]); + $tagLine = sprintf('User #%d (%s) has tag #%d ("%s") which has no transaction journals.', + $user->id, $user->email, $tag->id, $tag->tag); + + $this->artisan('firefly-iii:report-empty-objects') + ->expectsOutput($tagLine) + ->assertExitCode(0); + $tag->forceDelete(); + + // this method changes no objects so there is nothing to verify. + } + + /** + * Run basic test routine. + * + * @covers \FireflyIII\Console\Commands\Integrity\ReportEmptyObjects + */ + public function testHandleAccount(): void + { + $user = $this->user(); + $account = Account::create( + [ + 'user_id' => $user->id, + 'name' => 'Some account', + 'account_type_id' => 1, + ]); + $tagLine = sprintf('User #%d (%s) has account #%d ("%s") which has no transactions.', + $user->id, $user->email, $account->id, $account->name); + + $this->artisan('firefly-iii:report-empty-objects') + ->expectsOutput($tagLine) + ->assertExitCode(0); + $account->forceDelete(); + + // this method changes no objects so there is nothing to verify. + } + + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Integrity/ReportSumTest.php b/tests/Unit/Console/Commands/Integrity/ReportSumTest.php new file mode 100644 index 0000000000..1377a83231 --- /dev/null +++ b/tests/Unit/Console/Commands/Integrity/ReportSumTest.php @@ -0,0 +1,85 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Integrity; + + +use FireflyIII\Models\Transaction; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Tests\TestCase; + +/** + * Class ReportSumTest + */ +class ReportSumTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Integrity\ReportSum + */ + public function testHandle(): void + { + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + + $this->artisan('firefly-iii:report-sum') + ->expectsOutput(sprintf('Amount integrity OK for user #%d', $this->user()->id)) + ->assertExitCode(0); + + // this method changes no objects so there is nothing to verify. + } + + /** + * Create transaction to make balance uneven. + * @covers \FireflyIII\Console\Commands\Integrity\ReportSum + */ + public function testHandleUneven(): void + { + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $this->getRandomWithdrawal()->id, + 'user_id' => 1, + 'account_id' => $this->getRandomAsset()->id, + 'amount' => 10, + ] + ); + + $repository = $this->mock(UserRepositoryInterface::class); + $repository->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + + $this->artisan('firefly-iii:report-sum') + ->expectsOutput(sprintf('Error: Transactions for user #%d (%s) are off by %s!', $this->user()->id, $this->user()->email, '10.0')) + ->assertExitCode(0); + $transaction->forceDelete(); + + // this method changes no objects so there is nothing to verify. + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Tools/ApplyRulesTest.php b/tests/Unit/Console/Commands/Tools/ApplyRulesTest.php new file mode 100644 index 0000000000..e43e604854 --- /dev/null +++ b/tests/Unit/Console/Commands/Tools/ApplyRulesTest.php @@ -0,0 +1,404 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Tools; + + +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Rule\RuleRepositoryInterface; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; +use FireflyIII\TransactionRules\Engine\RuleEngine; +use Illuminate\Support\Collection; +use Log; +use Tests\TestCase; + +/** + * Class ApplyRulesTest + */ +class ApplyRulesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Basic call with everything perfect (and ALL rules). + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandle(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $ruleEngine = $this->mock(RuleEngine::class); + + // data + $asset = $this->getRandomAsset(); + $journal = $this->getRandomWithdrawal(); + $group = $this->user()->ruleGroups()->first(); + $rule = $this->user()->rules()->first(); + $groups = new Collection([$group]); + $rules = new Collection([$rule]); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([1])->andReturn($asset); + $journalRepos->shouldReceive('firstNull')->atLeast()->once()->andReturn($journal); + + $ruleGroupRepos->shouldReceive('getActiveGroups')->atLeast()->once()->andReturn($groups); + $ruleGroupRepos->shouldReceive('getActiveStoreRules')->atLeast()->once()->andReturn($rules); + + + $collector->shouldReceive('setUser')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setAccounts')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setRange')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([[], [], []]); + + $ruleEngine->shouldReceive('setUser')->atLeast()->once(); + $ruleEngine->shouldReceive('setRulesToApply')->atLeast()->once(); + $ruleEngine->shouldReceive('processJournalArray')->times(3); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=1', + '--all_rules', + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('Will apply 1 rule(s) to 3 transaction(s).') + ->expectsOutput('Done!') + ->assertExitCode(0); + + // this method changes no objects so there is nothing to verify. + } + + /** + * Basic call with everything perfect (and ALL rules), but no rules will be selected. + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandEmptye(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $ruleEngine = $this->mock(RuleEngine::class); + + // data + $asset = $this->getRandomAsset(); + $journal = $this->getRandomWithdrawal(); + $group = $this->user()->ruleGroups()->first(); + $groups = new Collection([$group]); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([1])->andReturn($asset); + $journalRepos->shouldReceive('firstNull')->atLeast()->once()->andReturn($journal); + + $ruleGroupRepos->shouldReceive('getActiveGroups')->atLeast()->once()->andReturn($groups); + $ruleGroupRepos->shouldReceive('getActiveStoreRules')->atLeast()->once()->andReturn(new Collection); + + + $collector->shouldReceive('setUser')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setAccounts')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setRange')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([[], [], []]); + + $ruleEngine->shouldReceive('setUser')->atLeast()->once(); + $ruleEngine->shouldReceive('setRulesToApply')->atLeast()->once(); + $ruleEngine->shouldReceive('processJournalArray')->times(3); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=1', + '--all_rules', + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('No rules or rule groups have been included.') + ->expectsOutput('Done!') + ->assertExitCode(0); + } + + + /** + * Basic call with everything perfect (and ALL rules) and dates. + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandleDate(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $ruleEngine = $this->mock(RuleEngine::class); + + // data + $asset = $this->getRandomAsset(); + $group = $this->user()->ruleGroups()->first(); + $rule = $this->user()->rules()->first(); + $groups = new Collection([$group]); + $rules = new Collection([$rule]); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([1])->andReturn($asset); + + $ruleGroupRepos->shouldReceive('getActiveGroups')->atLeast()->once()->andReturn($groups); + $ruleGroupRepos->shouldReceive('getActiveStoreRules')->atLeast()->once()->andReturn($rules); + + + $collector->shouldReceive('setUser')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setAccounts')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setRange')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([[], [], []]); + + $ruleEngine->shouldReceive('setUser')->atLeast()->once(); + $ruleEngine->shouldReceive('setRulesToApply')->atLeast()->once(); + $ruleEngine->shouldReceive('processJournalArray')->times(3); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=1', + '--all_rules', + '--start_date=2019-01-31', + '--end_date=2019-01-01', + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('Will apply 1 rule(s) to 3 transaction(s).') + ->expectsOutput('Done!') + ->assertExitCode(0); + } + + /** + * Will submit some rules to apply. + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandleRules(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $ruleEngine = $this->mock(RuleEngine::class); + + // data + $asset = $this->getRandomAsset(); + $journal = $this->getRandomWithdrawal(); + $group = $this->user()->ruleGroups()->first(); + $groups = new Collection([$group]); + $activeRule = $this->user()->rules()->where('active', 1)->inRandomOrder()->first(); + $inactiveRule = $this->user()->rules()->where('active', 0)->inRandomOrder()->first(); + $rules = new Collection([$activeRule]); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([1])->andReturn($asset); + $journalRepos->shouldReceive('firstNull')->atLeast()->once()->andReturn($journal); + + $ruleRepos->shouldReceive('find')->atLeast()->once()->withArgs([$activeRule->id])->andReturn($activeRule); + $ruleRepos->shouldReceive('find')->atLeast()->once()->withArgs([$inactiveRule->id])->andReturn($inactiveRule); + + $ruleGroupRepos->shouldReceive('getActiveGroups')->atLeast()->once()->andReturn($groups); + $ruleGroupRepos->shouldReceive('getActiveStoreRules')->atLeast()->once()->andReturn($rules); + + + $collector->shouldReceive('setUser')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setAccounts')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setRange')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([[], [], []]); + + $ruleEngine->shouldReceive('setUser')->atLeast()->once(); + $ruleEngine->shouldReceive('setRulesToApply')->atLeast()->once(); + $ruleEngine->shouldReceive('processJournalArray')->times(3); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=1', + sprintf('--rules=%d,%d', $activeRule->id, $inactiveRule->id), + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('Will apply 1 rule(s) to 3 transaction(s).') + ->expectsOutput('Done!') + ->assertExitCode(0); + } + + /** + * Basic call with two rule groups. One active, one inactive. + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandleRuleGroups(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $ruleEngine = $this->mock(RuleEngine::class); + + $activeGroup = $this->user()->ruleGroups()->where('active', 1)->inRandomOrder()->first(); + $inactiveGroup = $this->user()->ruleGroups()->where('active', 0)->inRandomOrder()->first(); + + // data + $asset = $this->getRandomAsset(); + $journal = $this->getRandomWithdrawal(); + $rule = $this->user()->rules()->first(); + $groups = new Collection([$activeGroup]); + $rules = new Collection([$rule]); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([1])->andReturn($asset); + $journalRepos->shouldReceive('firstNull')->atLeast()->once()->andReturn($journal); + + $ruleGroupRepos->shouldReceive('getActiveGroups')->atLeast()->once()->andReturn($groups); + $ruleGroupRepos->shouldReceive('getActiveStoreRules')->atLeast()->once()->andReturn($rules); + $ruleGroupRepos->shouldReceive('find')->atLeast()->once()->withArgs([$activeGroup->id])->andReturn($activeGroup); + $ruleGroupRepos->shouldReceive('find')->atLeast()->once()->withArgs([$inactiveGroup->id])->andReturn($inactiveGroup); + + $collector->shouldReceive('setUser')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setAccounts')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('setRange')->atLeast()->once()->andReturnSelf(); + $collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([[], [], []]); + + $ruleEngine->shouldReceive('setUser')->atLeast()->once(); + $ruleEngine->shouldReceive('setRulesToApply')->atLeast()->once(); + $ruleEngine->shouldReceive('processJournalArray')->times(3); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=1', + sprintf('--rule_groups=%d,%d', $activeGroup->id, $inactiveGroup->id), + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput(sprintf('Will ignore inactive rule group #%d ("%s")', $inactiveGroup->id, $inactiveGroup->title)) + // one rule out of 2 groups: + ->expectsOutput('Will apply 1 rule(s) to 3 transaction(s).') + ->expectsOutput('Done!') + ->assertExitCode(0); + } + + /** + * Basic call but no accounts submitted. + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandleNoAccounts(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $this->mock(JournalRepositoryInterface::class); + $this->mock(GroupCollectorInterface::class); + $this->mock(AccountRepositoryInterface::class); + $this->mock(RuleEngine::class); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=', + '--all_rules', + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('Please use the --accounts option to indicate the accounts to apply rules to.') + ->assertExitCode(1); + } + + /** + * Basic call but only one expense account submitted + * + * @covers \FireflyIII\Console\Commands\Tools\ApplyRules + */ + public function testHandleExpenseAccounts(): void + { + $ruleRepos = $this->mock(RuleRepositoryInterface::class); + $ruleGroupRepos = $this->mock(RuleGroupRepositoryInterface::class); + $this->mock(JournalRepositoryInterface::class); + $this->mock(GroupCollectorInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $this->mock(RuleEngine::class); + + // data + $expense = $this->getRandomExpense(); + + // expected calls: + $ruleRepos->shouldReceive('setUser')->atLeast()->once(); + $ruleGroupRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('findNull')->atLeast()->once()->withArgs([$expense->id])->andReturn($expense); + + + $parameters = [ + '--user=1', + '--token=token', + '--accounts=' . $expense->id, + '--all_rules', + ]; + + $this->artisan('firefly-iii:apply-rules ' . implode(' ', $parameters)) + ->expectsOutput('Please make sure all accounts in --accounts are asset accounts or liabilities.') + ->assertExitCode(1); + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Upgrade/AccountCurrenciesTest.php b/tests/Unit/Console/Commands/Upgrade/AccountCurrenciesTest.php new file mode 100644 index 0000000000..7c8df7d964 --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/AccountCurrenciesTest.php @@ -0,0 +1,341 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Upgrade; + + +use FireflyConfig; +use FireflyIII\Models\AccountMeta; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Preference; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Mockery; +use Preferences; +use Tests\TestCase; + +/** + * Class AccountCurrenciesTest + */ +class AccountCurrenciesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Perfect run without opening balance. + * + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandle(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'EUR'; + $account = $this->getRandomAsset(); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('1'); + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn(null); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput('All accounts are OK.') + ->assertExitCode(0); + + // nothing changed, so nothing to verify. + } + + /** + * Perfect run without opening balance. + * + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleNotNull(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'EUR'; + $account = $this->getRandomAsset(); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $journal = $this->getRandomWithdrawal(); + + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + + // account reports USD + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('2'); + // journal is EUR. + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn($journal); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)) + ->assertExitCode(0); + + // check if currency has been changed for the journal + transactions. + $this->assertCount(1, TransactionJournal::where('id', $journal->id)->where('transaction_currency_id', 2)->get()); + $this->assertCount(2, Transaction::where('transaction_journal_id', $journal->id)->where('transaction_currency_id', 2)->get()); + } + + /** + * Perfect run with opening balance. + * + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleOpeningBalance(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'EUR'; + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $journal = $this->getRandomWithdrawal(); + $account = $this->getRandomAsset(); + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('1'); + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn($journal); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput('All accounts are OK.') + ->assertExitCode(0); + + // nothing changed, dont check output. + } + + /** + * Perfect run with opening balance with different currencies + * + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleDifferent(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'USD'; + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $journal = $this->getRandomWithdrawal(); + $account = $this->getRandomAsset(); + $euro = TransactionCurrency::where('code', 'EUR')->first(); + + // delete meta data of account just in case: + AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete(); + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('0'); + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn($journal); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput(sprintf('Account #%d ("%s") now has a currency setting (#%d).', $account->id, $account->name, $euro->id)) + ->assertExitCode(0); + + // verify account meta data change. + $this->assertCount(1, + AccountMeta::where('account_id', $account->id) + ->where('name', 'currency_id') + ->where('data', $euro->id)->get()); + } + + /** + * No known currency preferences. + * + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleZeroPreference(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'EUR'; + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $account = $this->getRandomAsset(); + $euro = TransactionCurrency::where('code', 'EUR')->first(); + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('0'); + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn(null); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput(sprintf('Account #%d ("%s") now has a currency setting (%s).', + $account->id, $account->name, $euro->code + )) + ->expectsOutput('Corrected 1 account(s).') + ->assertExitCode(0); + + $this->assertCount(1, AccountMeta::where('account_id', $account->id) + ->where('name', 'currency_id') + ->where('data', $euro->id)->get()); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleNoPreference(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = false; + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $account = $this->getRandomAsset(); + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->andReturn('1'); + $accountRepos->shouldReceive('getOpeningBalance')->atLeast()->once()->andReturn(null); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput('All accounts are OK.') + ->assertExitCode(0); + // nothing changed, so nothing to verify. + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleInvalidPreference(): void + { + $false = new Configuration; + $false->data = false; + $pref = new Preference; + $pref->data = 'ABC'; + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $userRepos = $this->mock(UserRepositoryInterface::class); + $account = $this->getRandomAsset(); + + // mock calls + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $accountRepos->shouldReceive('getAccountsByType')->atLeast()->once()->andReturn(new Collection([$account])); + $userRepos->shouldReceive('all')->atLeast()->once()->andReturn(new Collection([$this->user()])); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput('User has a preference for "ABC", but this currency does not exist.') + ->assertExitCode(0); + + // nothing changed, so nothing to verify. + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\AccountCurrencies + */ + public function testHandleAlreadyExecuted(): void + { + $true = new Configuration; + $true->data = true; + $pref = new Preference; + $pref->data = 'EUR'; + $this->mock(AccountRepositoryInterface::class); + $this->mock(UserRepositoryInterface::class); + + // check config + FireflyConfig::shouldReceive('get')->withArgs(['4780_account_currencies', false])->andReturn($true); + FireflyConfig::shouldReceive('set')->withArgs(['4780_account_currencies', true]); + + // check preferences: + Preferences::shouldReceive('getForUser')->withArgs([Mockery::any(), 'currencyPreference', 'EUR'])->andReturn($pref); + + $this->artisan('firefly-iii:account-currencies') + ->expectsOutput('This command has already been executed.') + ->assertExitCode(0); + // nothing changed, so nothing to verify. + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Upgrade/BackToJournalsTest.php b/tests/Unit/Console/Commands/Upgrade/BackToJournalsTest.php new file mode 100644 index 0000000000..203471e974 --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/BackToJournalsTest.php @@ -0,0 +1,244 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Upgrade; + + +use FireflyConfig; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Transaction; +use Log; +use Tests\TestCase; + +/** + * Class BackToJournalsTest + */ +class BackToJournalsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Perfect run. Will report on nothing. + * + * @covers \FireflyIII\Console\Commands\Upgrade\BackToJournals + */ + public function testHandle(): void + { + // verify preference: + $false = new Configuration; + $false->data = false; + $true = new Configuration; + $true->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['4780_back_to_journals', false])->andReturn($false); + FireflyConfig::shouldReceive('get')->withArgs(['4780_migrated_to_groups', false])->andReturn($true); + + // set new preference after running: + FireflyConfig::shouldReceive('set')->withArgs(['4780_back_to_journals', true]); + + $this->artisan('firefly-iii:back-to-journals') + ->expectsOutput('Check 0 transaction journal(s) for budget info.') + ->expectsOutput('Check 0 transaction journal(s) for category info.') + ->assertExitCode(0); + + } + + /** + * Transaction has a budget, journal doesn't. + * + * @covers \FireflyIII\Console\Commands\Upgrade\BackToJournals + */ + public function testHandleBudget(): void + { + $journal = $this->getRandomWithdrawal(); + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Budget $budget */ + $budget = $this->user()->budgets()->first(); + $transaction->budgets()->sync([$budget->id]); + $journal->budgets()->sync([]); + $journal->save(); + $transaction->save(); + + + // verify preference: + $false = new Configuration; + $false->data = false; + $true = new Configuration; + $true->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['4780_back_to_journals', false])->andReturn($false); + FireflyConfig::shouldReceive('get')->withArgs(['4780_migrated_to_groups', false])->andReturn($true); + + // set new preference after running: + FireflyConfig::shouldReceive('set')->withArgs(['4780_back_to_journals', true]); + + $this->artisan('firefly-iii:back-to-journals') + ->expectsOutput('Check 1 transaction journal(s) for budget info.') + ->expectsOutput('Check 0 transaction journal(s) for category info.') + ->assertExitCode(0); + + // transaction should have no budget: + $this->assertEquals(0, $transaction->budgets()->count()); + // journal should have one. + $this->assertEquals(1, $journal->budgets()->count()); + // should be $budget: + $this->assertEquals($budget->id, $journal->budgets()->first()->id); + } + + /** + * Transaction has a category, journal doesn't. + * + * @covers \FireflyIII\Console\Commands\Upgrade\BackToJournals + */ + public function testHandleCategory(): void + { + $journal = $this->getRandomWithdrawal(); + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Category $category */ + $category = $this->user()->categories()->first(); + $transaction->categories()->sync([$category->id]); + $journal->categories()->sync([]); + $journal->save(); + $transaction->save(); + + // verify preference: + $false = new Configuration; + $false->data = false; + $true = new Configuration; + $true->data = true; + + FireflyConfig::shouldReceive('get')->withArgs(['4780_back_to_journals', false])->andReturn($false); + FireflyConfig::shouldReceive('get')->withArgs(['4780_migrated_to_groups', false])->andReturn($true); + + // set new preference after running: + FireflyConfig::shouldReceive('set')->withArgs(['4780_back_to_journals', true]); + + $this->artisan('firefly-iii:back-to-journals') + ->expectsOutput('Check 0 transaction journal(s) for budget info.') + ->expectsOutput('Check 1 transaction journal(s) for category info.') + ->assertExitCode(0); + + // transaction should have no category: + $this->assertEquals(0, $transaction->categories()->count()); + // journal should have one. + $this->assertEquals(1, $journal->categories()->count()); + // should be $category: + $this->assertEquals($category->id, $journal->categories()->first()->id); + } + + /** + * Transaction has a budget, journal has another + * + * @covers \FireflyIII\Console\Commands\Upgrade\BackToJournals + */ + public function testHandleDifferentBudget(): void + { + $journal = $this->getRandomWithdrawal(); + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Budget $budget */ + $budget = $this->user()->budgets()->first(); + $otherBudget = $this->user()->budgets()->where('id', '!=', $budget->id)->first(); + $transaction->budgets()->sync([$budget->id]); + $journal->budgets()->sync([$otherBudget->id]); + $journal->save(); + $transaction->save(); + + + // verify preference: + $false = new Configuration; + $false->data = false; + $true = new Configuration; + $true->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['4780_back_to_journals', false])->andReturn($false); + FireflyConfig::shouldReceive('get')->withArgs(['4780_migrated_to_groups', false])->andReturn($true); + + // set new preference after running: + FireflyConfig::shouldReceive('set')->withArgs(['4780_back_to_journals', true]); + + $this->artisan('firefly-iii:back-to-journals') + ->expectsOutput('Check 1 transaction journal(s) for budget info.') + ->expectsOutput('Check 0 transaction journal(s) for category info.') + ->assertExitCode(0); + + // transaction should have no budget: + $this->assertEquals(0, $transaction->budgets()->count()); + // journal should have one. + $this->assertEquals(1, $journal->budgets()->count()); + // should be $budget: + $this->assertEquals($budget->id, $journal->budgets()->first()->id); + + } + + + /** + * Transaction has a category, journal has another + * + * @covers \FireflyIII\Console\Commands\Upgrade\BackToJournals + */ + public function testHandleDifferentCategory(): void + { + $journal = $this->getRandomWithdrawal(); + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Category $category */ + $category = $this->user()->categories()->first(); + $otherCategory = $this->user()->categories()->where('id', '!=', $category->id)->first(); + $transaction->categories()->sync([$category->id]); + $journal->categories()->sync([$otherCategory->id]); + $journal->save(); + $transaction->save(); + + // verify preference: + $false = new Configuration; + $false->data = false; + $true = new Configuration; + $true->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['4780_back_to_journals', false])->andReturn($false); + FireflyConfig::shouldReceive('get')->withArgs(['4780_migrated_to_groups', false])->andReturn($true); + + // set new preference after running: + FireflyConfig::shouldReceive('set')->withArgs(['4780_back_to_journals', true]); + + $this->artisan('firefly-iii:back-to-journals') + ->expectsOutput('Check 0 transaction journal(s) for budget info.') + ->expectsOutput('Check 1 transaction journal(s) for category info.') + ->assertExitCode(0); + + // transaction should have no category: + $this->assertEquals(0, $transaction->categories()->count()); + // journal should have one. + $this->assertEquals(1, $journal->categories()->count()); + // should be $category: + $this->assertEquals($category->id, $journal->categories()->first()->id); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Upgrade/BudgetLimitCurrencyTest.php b/tests/Unit/Console/Commands/Upgrade/BudgetLimitCurrencyTest.php new file mode 100644 index 0000000000..f2d55e6eab --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/BudgetLimitCurrencyTest.php @@ -0,0 +1,85 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Upgrade; + + +use FireflyConfig; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\Configuration; +use Log; +use Tests\TestCase; + +/** + * Class BudgetLimitCurrencyTest + */ +class BudgetLimitCurrencyTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\BudgetLimitCurrency + */ + public function testHandle(): void + { + $this->artisan('firefly-iii:bl-currency') + ->assertExitCode(0); + } + + /** + * Create a bad budget limit. + * @covers \FireflyIII\Console\Commands\Upgrade\BudgetLimitCurrency + */ + public function testHandleBadLimit(): void + { + $false = new Configuration; + $false->data = false; + $budget = $this->user()->budgets()->first(); + $limit = BudgetLimit::create( + [ + 'budget_id' => $budget->id, + 'amount' => '10', + 'start_date' => '2019-01-01', + 'end_date' => '2019-01-31', + ]); + + FireflyConfig::shouldReceive('get')->withArgs(['4780_bl_currency', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_bl_currency', true]); + + $this->artisan('firefly-iii:bl-currency') + ->expectsOutput( + sprintf('Budget limit #%d (part of budget "%s") now has a currency setting (%s).', + $limit->id, $budget->name, 'Euro' + ) + ) + ->assertExitCode(0); + + // assume currency is filled in. + $this->assertCount(1, BudgetLimit::where('id', $limit->id)->where('transaction_currency_id', 1)->get()); + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Upgrade/CCLiabilitiesTest.php b/tests/Unit/Console/Commands/Upgrade/CCLiabilitiesTest.php new file mode 100644 index 0000000000..ef093beb38 --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/CCLiabilitiesTest.php @@ -0,0 +1,129 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Upgrade; + + +use FireflyConfig; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Configuration; +use Log; +use Tests\TestCase; + +/** + * Class CCLiabilitiesTest + */ +class CCLiabilitiesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\CCLiabilities + */ + public function testHandle(): void + { + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_cc_liabilities', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_cc_liabilities', true]); + + + $this->artisan('firefly-iii:cc-liabilities') + ->expectsOutput('No incorrectly stored credit card liabilities.') + ->assertExitCode(0); + + // nothing changed, so nothing to verify. + } + + /** + * Add type to make the script run. + * + * @covers \FireflyIII\Console\Commands\Upgrade\CCLiabilities + */ + public function testHandleEmpty(): void + { + $type = AccountType::create( + [ + 'type' => AccountType::CREDITCARD, + ] + ); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_cc_liabilities', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_cc_liabilities', true]); + + + $this->artisan('firefly-iii:cc-liabilities') + ->expectsOutput('No incorrectly stored credit card liabilities.') + ->assertExitCode(0); + $type->forceDelete(); + // nothing changed, so nothing to verify. + } + + /** + * Add some things to make it trigger. + * + * @covers \FireflyIII\Console\Commands\Upgrade\CCLiabilities + */ + public function testHandleCase(): void + { + $type = AccountType::create( + [ + 'type' => AccountType::CREDITCARD, + ] + ); + + $account = Account::create( + [ + 'name' => 'CC', + 'user_id' => 1, + 'account_type_id' => $type->id, + + ] + ); + + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_cc_liabilities', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_cc_liabilities', true]); + + + $this->artisan('firefly-iii:cc-liabilities') + ->expectsOutput(sprintf('Converted credit card liability account "%s" (#%d) to generic debt liability.', $account->name, $account->id)) + ->expectsOutput('Credit card liability types are no longer supported and have been converted to generic debts. See: http://bit.ly/FF3-credit-cards') + ->assertExitCode(0); + + // verify new type. + $this->assertCount(0, Account::where('id', $account->id)->where('account_type_id', $type->id)->get()); + + $account->forceDelete(); + $type->forceDelete(); + + } +} \ No newline at end of file diff --git a/tests/Unit/Console/Commands/Upgrade/JournalCurrenciesTest.php b/tests/Unit/Console/Commands/Upgrade/JournalCurrenciesTest.php new file mode 100644 index 0000000000..9b7c00a2eb --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/JournalCurrenciesTest.php @@ -0,0 +1,354 @@ +. + */ + +namespace Tests\Unit\Console\Commands\Upgrade; + + +use FireflyConfig; +use FireflyIII\Models\Account; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Mockery; +use Tests\TestCase; + +/** + * Class JournalCurrenciesTest + */ +class JournalCurrenciesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + /** + * Basic run. Would not change anything. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandle(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + + // update transfer if necessary for the test: + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn(new Collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput('All transactions are correct.') + ->assertExitCode(0); + // nothing changed, so no verification. + + } + + /** + * Submit a single transfer which has no issues. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandleTransfer(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + $transfer = $this->getRandomTransfer(); + + // update transfer if necessary for the test: + $collection = new Collection([$transfer]); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $currencyRepos->shouldReceive('setUser')->atLeast()->once(); + + // return single tranfer + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn($collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->withArgs([Mockery::any(), 'currency_id'])->andReturn($euro->id); + $currencyRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($euro); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput('Verified 1 transaction(s) and journal(s).') + ->assertExitCode(0); + // nothing changed, so no verification. + } + + /** + * Submit a single transfer where the source account has no currency preference. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandleTransferSourceNoPref(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + $transfer = $this->getRandomTransfer(); + + // edit source to remove currency preference: + /** @var Account $source */ + $source = $transfer->transactions()->where('amount', '<', 0)->first()->account; +// AccountMeta::where('account_id', $source->id)->where('name', 'currency_id')->delete(); + + // update transfer if necessary for the test: + $collection = new Collection([$transfer]); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $currencyRepos->shouldReceive('setUser')->atLeast()->once(); + + // return single transfer + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn($collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + // return NULL for first currency ID and currency. + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->withArgs([Mockery::any(), 'currency_id'])->andReturnNull(); + $currencyRepos->shouldReceive('findNull')->atLeast()->once()->andReturnNull(); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput(sprintf('Account #%d ("%s") must have currency preference but has none.', $source->id, $source->name)) + ->assertExitCode(0); + // nothing changed, so no verification. + + } + + /** + * Submit a single transfer where the source transaction has no currency set. + * Because this is not done over repositories, we must edit the DB. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandleTransferSourceNoCurrency(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + $transfer = $this->getRandomTransfer(); + /** @var Transaction $source */ + $source = $transfer->transactions()->where('amount', '<', 0)->first(); + $source->transaction_currency_id = null; + $source->save(); + + // update transfer if necessary for the test: + $collection = new Collection([$transfer]); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $currencyRepos->shouldReceive('setUser')->atLeast()->once(); + + // return single tranfer + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn($collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->withArgs([Mockery::any(), 'currency_id'])->andReturn($euro->id); + $currencyRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($euro); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput(sprintf('Transaction #%d has no currency setting, now set to %s.', $source->id, $euro->code)) + ->expectsOutput('Verified 2 transaction(s) and journal(s).') + ->assertExitCode(0); + + // check transaction + $this->assertCount(1, Transaction::where('id', $source->id)->where('transaction_currency_id', $euro->id)->get()); + } + + /** + * Submit a single transfer where the source transaction has a different currency than the source account does. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandleMismatchedTransfer(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + $usd = TransactionCurrency::where('code', 'USD')->first(); + $transfer = $this->getRandomTransfer(); + + /** @var Transaction $source */ + $source = $transfer->transactions()->where('amount', '<', 0)->first(); + $source->transaction_currency_id = $usd->id; + $source->save(); + + // update transfer if necessary for the test: + $collection = new Collection([$transfer]); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $currencyRepos->shouldReceive('setUser')->atLeast()->once(); + + // return single tranfer + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn($collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->withArgs([Mockery::any(), 'currency_id'])->andReturn($euro->id); + $currencyRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($euro); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput( + sprintf( + 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', + $source->id, + $source->transaction_currency_id, + $euro->id, + $source->amount + ) + ) + ->expectsOutput('Verified 2 transaction(s) and journal(s).') + ->assertExitCode(0); + // nothing changed, so no verification. + } + + /** + * Submit a single transfer where the destination account has no currency preference. + * + * @covers \FireflyIII\Console\Commands\Upgrade\JournalCurrencies + */ + public function testHandleTransferNoDestinationCurrency(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $euro = TransactionCurrency::find(1); + $transfer = $this->getRandomTransfer(); + + /** @var Account $destination */ + $destination = $transfer->transactions()->where('amount', '>', 0)->first()->account; + + // update transfer if necessary for the test: + $collection = new Collection([$transfer]); + $false = new Configuration; + $false->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['4780_journal_currencies', false])->andReturn($false); + FireflyConfig::shouldReceive('set')->withArgs(['4780_journal_currencies', true]); + + // mock stuff + $accountRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $currencyRepos->shouldReceive('setUser')->atLeast()->once(); + + // return single tranfer + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::TRANSFER]]) + ->andReturn($collection); + + // for the "other journals" check, return nothing. + $journalRepos->shouldReceive('getAllJournals')->atLeast()->once() + ->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]]) + ->andReturn(new Collection); + + $accountRepos->shouldReceive('getMetaValue')->atLeast()->once()->withArgs([Mockery::any(), 'currency_id'])->andReturn($euro->id, 0); + $currencyRepos->shouldReceive('findNull')->atLeast()->once()->andReturn($euro, null); + + // transaction would be verified, nothing more. + $this->artisan('firefly-iii:journal-currencies') + ->expectsOutput(sprintf('Account #%d ("%s") must have currency preference but has none.', $destination->id, $destination->name)) + ->assertExitCode(0); + // nothing changed, so no verification. + } + + +} \ No newline at end of file