From e5a77a86f965ebad7c40d0a36a1f7623b7c64a07 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 7 May 2023 20:17:29 +0200 Subject: [PATCH] Fix a few commands. --- .ci/php-cs-fixer/composer.lock | 48 +- .../Commands/Correction/CorrectAmounts.php | 38 +- .../Commands/Correction/CorrectDatabase.php | 2 +- .../CorrectOpeningBalanceCurrencies.php | 2 +- .../Correction/DeleteEmptyJournals.php | 54 +- .../Correction/DeleteOrphanedTransactions.php | 65 +- app/Console/Commands/Correction/FixIbans.php | 22 +- .../Correction/FixRecurringTransactions.php | 52 +- .../Correction/FixTransactionTypes.php | 72 +- .../Correction/TriggerCreditCalculation.php | 22 +- .../Integrity/CreateGroupMemberships.php | 66 +- .../Commands/Integrity/ReportEmptyObjects.php | 90 +-- .../Commands/Integrity/ReportIntegrity.php | 2 +- .../Commands/Integrity/RestoreOAuthKeys.php | 64 +- .../Commands/System/ForceDecimalSize.php | 614 +++++++++++++++++- .../Commands/System/ForceMigration.php | 13 +- .../System/UpgradeFireflyInstructions.php | 91 ++- app/Console/Commands/Tools/Cron.php | 114 ++-- .../Commands/Upgrade/AccountCurrencies.php | 106 +-- .../Upgrade/AppendBudgetLimitPeriods.php | 48 +- .../Commands/Upgrade/BackToJournals.php | 100 +-- .../Commands/Upgrade/DecryptDatabase.php | 150 ++--- .../Upgrade/MigrateRecurrenceMeta.php | 30 +- .../Upgrade/MigrateRecurrenceType.php | 36 +- .../Commands/Upgrade/MigrateTagLocations.php | 40 +- .../Commands/Upgrade/MigrateToRules.php | 94 +-- .../Upgrade/TransactionIdentifier.php | 114 ++-- .../Commands/Upgrade/UpgradeDatabase.php | 2 +- .../Commands/Upgrade/UpgradeLiabilities.php | 154 ++--- .../Upgrade/UpgradeLiabilitiesEight.php | 276 ++++---- .../Events/Model/BudgetLimitHandler.php | 6 +- app/Models/AutoBudget.php | 2 + .../V2/TransactionGroupTransformer.php | 3 +- 33 files changed, 1575 insertions(+), 1017 deletions(-) diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index aa241e687b..e445219f5b 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -677,16 +677,16 @@ }, { "name": "sebastian/diff", - "version": "5.0.1", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02" + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/aae9a0a43bff37bd5d8d0311426c87bf36153f02", - "reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", "shasum": "" }, "require": { @@ -732,7 +732,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" }, "funding": [ { @@ -740,20 +740,20 @@ "type": "github" } ], - "time": "2023-03-23T05:12:41+00:00" + "time": "2023-05-01T07:48:21+00:00" }, { "name": "symfony/console", - "version": "v6.2.8", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b" + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3582d68a64a86ec25240aaa521ec8bc2342b369b", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b", + "url": "https://api.github.com/repos/symfony/console/zipball/12288d9f4500f84a4d02254d4aa968b15488476f", + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f", "shasum": "" }, "require": { @@ -820,7 +820,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.8" + "source": "https://github.com/symfony/console/tree/v6.2.10" }, "funding": [ { @@ -836,7 +836,7 @@ "type": "tidelift" } ], - "time": "2023-03-29T21:42:15+00:00" + "time": "2023-04-28T13:37:43+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1069,16 +1069,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.2.7", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3" + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/82b6c62b959f642d000456f08c6d219d749215b3", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd588debf7d1bc16a2c84b4b3b71145d9946b894", + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894", "shasum": "" }, "require": { @@ -1112,7 +1112,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.2.7" + "source": "https://github.com/symfony/filesystem/tree/v6.2.10" }, "funding": [ { @@ -1128,7 +1128,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-04-18T13:46:08+00:00" }, { "name": "symfony/finder", @@ -1755,16 +1755,16 @@ }, { "name": "symfony/process", - "version": "v6.2.8", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416" + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/75ed64103df4f6615e15a7fe38b8111099f47416", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416", + "url": "https://api.github.com/repos/symfony/process/zipball/b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", "shasum": "" }, "require": { @@ -1796,7 +1796,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.2.8" + "source": "https://github.com/symfony/process/tree/v6.2.10" }, "funding": [ { @@ -1812,7 +1812,7 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:20:02+00:00" + "time": "2023-04-18T13:56:57+00:00" }, { "name": "symfony/service-contracts", diff --git a/app/Console/Commands/Correction/CorrectAmounts.php b/app/Console/Commands/Correction/CorrectAmounts.php index 56692c0821..b95abf7b97 100644 --- a/app/Console/Commands/Correction/CorrectAmounts.php +++ b/app/Console/Commands/Correction/CorrectAmounts.php @@ -177,25 +177,6 @@ class CorrectAmounts extends Command $this->line(sprintf('Corrected %d currency exchange rate(s).', $count)); } - /** - * @return void - */ - private function fixRepetitions(): void - { - $set = PiggyBankRepetition::where('currentamount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->info('Correct: All piggy bank repetition amounts are positive.'); - return; - } - /** @var PiggyBankRepetition $item */ - foreach ($set as $item) { - $item->currentamount = app('steam')->positive((string)$item->currentamount); - $item->save(); - } - $this->line(sprintf('Corrected %d piggy bank repetition amount(s).', $count)); - } - /** * @return void */ @@ -237,6 +218,25 @@ class CorrectAmounts extends Command $this->line(sprintf('Corrected %d recurring transaction amount(s).', $count)); } + /** + * @return void + */ + private function fixRepetitions(): void + { + $set = PiggyBankRepetition::where('currentamount', '<', 0)->get(); + $count = $set->count(); + if (0 === $count) { + $this->info('Correct: All piggy bank repetition amounts are positive.'); + return; + } + /** @var PiggyBankRepetition $item */ + foreach ($set as $item) { + $item->currentamount = app('steam')->positive((string)$item->currentamount); + $item->save(); + } + $this->line(sprintf('Corrected %d piggy bank repetition amount(s).', $count)); + } + /** * @return void */ diff --git a/app/Console/Commands/Correction/CorrectDatabase.php b/app/Console/Commands/Correction/CorrectDatabase.php index 6c8c9bf1d8..87dd43e36f 100644 --- a/app/Console/Commands/Correction/CorrectDatabase.php +++ b/app/Console/Commands/Correction/CorrectDatabase.php @@ -83,7 +83,7 @@ class CorrectDatabase extends Command 'firefly-iii:fix-frontpage-accounts', // new! 'firefly-iii:unify-group-accounts', - 'firefly-iii:trigger-credit-recalculation' + 'firefly-iii:trigger-credit-recalculation', ]; foreach ($commands as $command) { $this->line(sprintf('Now executing command "%s"', $command)); diff --git a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php b/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php index 7685e71bf1..3953eba431 100644 --- a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php +++ b/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php @@ -33,8 +33,8 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Illuminate\Console\Command; -use JsonException; use Illuminate\Support\Facades\Log; +use JsonException; /** * Class CorrectOpeningBalanceCurrencies diff --git a/app/Console/Commands/Correction/DeleteEmptyJournals.php b/app/Console/Commands/Correction/DeleteEmptyJournals.php index 45c3ce7aa7..e62b6be7d6 100644 --- a/app/Console/Commands/Correction/DeleteEmptyJournals.php +++ b/app/Console/Commands/Correction/DeleteEmptyJournals.php @@ -61,6 +61,33 @@ class DeleteEmptyJournals extends Command return 0; } + private function deleteEmptyJournals(): void + { + $start = microtime(true); + $count = 0; + $set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->groupBy('transaction_journals.id') + ->whereNull('transactions.transaction_journal_id') + ->get(['transaction_journals.id']); + + foreach ($set as $entry) { + try { + TransactionJournal::find($entry->id)->delete(); + } catch (QueryException $e) { + Log::info(sprintf('Could not delete entry: %s', $e->getMessage())); + } + + + $this->info(sprintf('Deleted empty transaction journal #%d', $entry->id)); + ++$count; + } + if (0 === $count) { + $this->info('No empty transaction journals.'); + } + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Verified empty journals in %s seconds', $end)); + } + /** * Delete transactions and their journals if they have an uneven number of transactions. */ @@ -91,31 +118,4 @@ class DeleteEmptyJournals extends Command $this->info('No uneven transaction journals.'); } } - - private function deleteEmptyJournals(): void - { - $start = microtime(true); - $count = 0; - $set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->groupBy('transaction_journals.id') - ->whereNull('transactions.transaction_journal_id') - ->get(['transaction_journals.id']); - - foreach ($set as $entry) { - try { - TransactionJournal::find($entry->id)->delete(); - } catch (QueryException $e) { - Log::info(sprintf('Could not delete entry: %s', $e->getMessage())); - } - - - $this->info(sprintf('Deleted empty transaction journal #%d', $entry->id)); - ++$count; - } - if (0 === $count) { - $this->info('No empty transaction journals.'); - } - $end = round(microtime(true) - $start, 2); - $this->info(sprintf('Verified empty journals in %s seconds', $end)); - } } diff --git a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php index 461dd0bda9..b559c43101 100644 --- a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php +++ b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php @@ -27,7 +27,6 @@ use Exception; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Log; use stdClass; /** @@ -66,6 +65,38 @@ class DeleteOrphanedTransactions extends Command return 0; } + /** + * + */ + private function deleteFromOrphanedAccounts(): void + { + $set + = Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') + ->whereNotNull('accounts.deleted_at') + ->get(['transactions.*']); + $count = 0; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + // delete journals + $journal = TransactionJournal::find((int)$transaction->transaction_journal_id); + if ($journal) { + $journal->delete(); + } + 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 + ) + ); + $count++; + } + if (0 === $count) { + $this->info('No orphaned accounts.'); + } + } + private function deleteOrphanedJournals(): void { $set = TransactionJournal::leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id') @@ -129,36 +160,4 @@ class DeleteOrphanedTransactions extends Command $this->info('No orphaned transactions.'); } } - - /** - * - */ - private function deleteFromOrphanedAccounts(): void - { - $set - = Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') - ->whereNotNull('accounts.deleted_at') - ->get(['transactions.*']); - $count = 0; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - // delete journals - $journal = TransactionJournal::find((int)$transaction->transaction_journal_id); - if ($journal) { - $journal->delete(); - } - 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 - ) - ); - $count++; - } - if (0 === $count) { - $this->info('No orphaned accounts.'); - } - } } diff --git a/app/Console/Commands/Correction/FixIbans.php b/app/Console/Commands/Correction/FixIbans.php index 18edde5bbc..a465259e94 100644 --- a/app/Console/Commands/Correction/FixIbans.php +++ b/app/Console/Commands/Correction/FixIbans.php @@ -69,30 +69,38 @@ class FixIbans extends Command { $set = []; /** @var Account $account */ - foreach($accounts as $account) { + foreach ($accounts as $account) { $userId = (int)$account->user_id; $set[$userId] = $set[$userId] ?? []; $iban = (string)$account->iban; - if('' === $iban) { + if ('' === $iban) { continue; } $type = $account->accountType->type; - if(in_array($type, [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], true)) { + if (in_array($type, [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], true)) { $type = 'liabilities'; } - if(array_key_exists($iban, $set[$userId])) { + if (array_key_exists($iban, $set[$userId])) { // iban already in use! two exceptions exist: - if( + if ( !(AccountType::EXPENSE === $set[$userId][$iban] && AccountType::REVENUE === $type) && // allowed combination !(AccountType::REVENUE === $set[$userId][$iban] && AccountType::EXPENSE === $type) // also allowed combination. ) { - $this->line(sprintf('IBAN "%s" is used more than once and will be removed from %s #%d ("%s")', $iban, $account->accountType->type, $account->id, $account->name)); + $this->line( + sprintf( + 'IBAN "%s" is used more than once and will be removed from %s #%d ("%s")', + $iban, + $account->accountType->type, + $account->id, + $account->name + ) + ); $account->iban = null; $account->save(); } } - if(!array_key_exists($iban, $set[$userId])) { + if (!array_key_exists($iban, $set[$userId])) { $set[$userId][$iban] = $type; } } diff --git a/app/Console/Commands/Correction/FixRecurringTransactions.php b/app/Console/Commands/Correction/FixRecurringTransactions.php index 99e1a08b18..05d182dfc8 100644 --- a/app/Console/Commands/Correction/FixRecurringTransactions.php +++ b/app/Console/Commands/Correction/FixRecurringTransactions.php @@ -70,19 +70,6 @@ class FixRecurringTransactions extends Command return 0; } - /** - * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is - * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should - * be called from the handle method instead of using the constructor to initialize the command. - * - - */ - private function stupidLaravel(): void - { - $this->recurringRepos = app(RecurringRepositoryInterface::class); - $this->userRepos = app(UserRepositoryInterface::class); - } - /** * */ @@ -95,19 +82,6 @@ class FixRecurringTransactions extends Command } } - /** - * @param User $user - */ - private function processUser(User $user): void - { - $this->recurringRepos->setUser($user); - $recurrences = $this->recurringRepos->get(); - /** @var Recurrence $recurrence */ - foreach ($recurrences as $recurrence) { - $this->processRecurrence($recurrence); - } - } - /** * @param Recurrence $recurrence */ @@ -140,4 +114,30 @@ class FixRecurringTransactions extends Command } } } + + /** + * @param User $user + */ + private function processUser(User $user): void + { + $this->recurringRepos->setUser($user); + $recurrences = $this->recurringRepos->get(); + /** @var Recurrence $recurrence */ + foreach ($recurrences as $recurrence) { + $this->processRecurrence($recurrence); + } + } + + /** + * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is + * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should + * be called from the handle method instead of using the constructor to initialize the command. + * + + */ + private function stupidLaravel(): void + { + $this->recurringRepos = app(RecurringRepositoryInterface::class); + $this->userRepos = app(UserRepositoryInterface::class); + } } diff --git a/app/Console/Commands/Correction/FixTransactionTypes.php b/app/Console/Commands/Correction/FixTransactionTypes.php index fef7952d9a..c818bd6ca1 100644 --- a/app/Console/Commands/Correction/FixTransactionTypes.php +++ b/app/Console/Commands/Correction/FixTransactionTypes.php @@ -78,6 +78,19 @@ class FixTransactionTypes extends Command return 0; } + /** + * @param TransactionJournal $journal + * @param string $expectedType + */ + private function changeJournal(TransactionJournal $journal, string $expectedType): void + { + $type = TransactionType::whereType($expectedType)->first(); + if (null !== $type) { + $journal->transaction_type_id = $type->id; + $journal->save(); + } + } + /** * Collect all transaction journals. * @@ -125,36 +138,6 @@ class FixTransactionTypes extends Command return false; } - /** - * @param TransactionJournal $journal - * - * @return Account - * @throws FireflyException - */ - private function getSourceAccount(TransactionJournal $journal): Account - { - $collection = $journal->transactions->filter( - static function (Transaction $transaction) { - return $transaction->amount < 0; - } - ); - if (0 === $collection->count()) { - throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id)); - } - if (1 !== $collection->count()) { - throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id)); - } - /** @var Transaction $transaction */ - $transaction = $collection->first(); - /** @var Account|null $account */ - $account = $transaction->account; - if (null === $account) { - throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id)); - } - - return $account; - } - /** * @param TransactionJournal $journal * @@ -187,14 +170,31 @@ class FixTransactionTypes extends Command /** * @param TransactionJournal $journal - * @param string $expectedType + * + * @return Account + * @throws FireflyException */ - private function changeJournal(TransactionJournal $journal, string $expectedType): void + private function getSourceAccount(TransactionJournal $journal): Account { - $type = TransactionType::whereType($expectedType)->first(); - if (null !== $type) { - $journal->transaction_type_id = $type->id; - $journal->save(); + $collection = $journal->transactions->filter( + static function (Transaction $transaction) { + return $transaction->amount < 0; + } + ); + if (0 === $collection->count()) { + throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id)); } + if (1 !== $collection->count()) { + throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id)); + } + /** @var Transaction $transaction */ + $transaction = $collection->first(); + /** @var Account|null $account */ + $account = $transaction->account; + if (null === $account) { + throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id)); + } + + return $account; } } diff --git a/app/Console/Commands/Correction/TriggerCreditCalculation.php b/app/Console/Commands/Correction/TriggerCreditCalculation.php index a0e7ca9d9d..14f86dd29f 100644 --- a/app/Console/Commands/Correction/TriggerCreditCalculation.php +++ b/app/Console/Commands/Correction/TriggerCreditCalculation.php @@ -39,17 +39,6 @@ class TriggerCreditCalculation extends Command return 0; } - private function processAccounts(): void - { - $accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') - ->whereIn('account_types.type', config('firefly.valid_liabilities')) - ->get(['accounts.*']); - foreach ($accounts as $account) { - Log::debug(sprintf('Processing account #%d ("%s")', $account->id, $account->name)); - $this->processAccount($account); - } - } - /** * @param Account $account * @return void @@ -61,4 +50,15 @@ class TriggerCreditCalculation extends Command $object->setAccount($account); $object->recalculate(); } + + private function processAccounts(): void + { + $accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + foreach ($accounts as $account) { + Log::debug(sprintf('Processing account #%d ("%s")', $account->id, $account->name)); + $this->processAccount($account); + } + } } diff --git a/app/Console/Commands/Integrity/CreateGroupMemberships.php b/app/Console/Commands/Integrity/CreateGroupMemberships.php index d83d8cfca7..768df35247 100644 --- a/app/Console/Commands/Integrity/CreateGroupMemberships.php +++ b/app/Console/Commands/Integrity/CreateGroupMemberships.php @@ -51,39 +51,6 @@ class CreateGroupMemberships extends Command */ protected $signature = 'firefly-iii:create-group-memberships'; - /** - * Execute the console command. - * - * @return int - * @throws FireflyException - */ - public function handle(): int - { - $start = microtime(true); - - $this->createGroupMemberships(); - - $end = round(microtime(true) - $start, 2); - $this->info(sprintf('Validated group memberships in %s seconds.', $end)); - - return 0; - } - - /** - * - * @throws FireflyException - */ - private function createGroupMemberships(): void - { - $users = User::get(); - /** @var User $user */ - foreach ($users as $user) { - Log::debug(sprintf('Manage group memberships for user #%d', $user->id)); - self::createGroupMembership($user); - Log::debug(sprintf('Done with user #%d', $user->id)); - } - } - /** * TODO move to helper. * @param User $user @@ -125,4 +92,37 @@ class CreateGroupMemberships extends Command Log::debug(sprintf('User #%d now has main group.', $user->id)); } + + /** + * Execute the console command. + * + * @return int + * @throws FireflyException + */ + public function handle(): int + { + $start = microtime(true); + + $this->createGroupMemberships(); + + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Validated group memberships in %s seconds.', $end)); + + return 0; + } + + /** + * + * @throws FireflyException + */ + private function createGroupMemberships(): void + { + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + Log::debug(sprintf('Manage group memberships for user #%d', $user->id)); + self::createGroupMembership($user); + Log::debug(sprintf('Done with user #%d', $user->id)); + } + } } diff --git a/app/Console/Commands/Integrity/ReportEmptyObjects.php b/app/Console/Commands/Integrity/ReportEmptyObjects.php index 73c8814261..ea3e2e342f 100644 --- a/app/Console/Commands/Integrity/ReportEmptyObjects.php +++ b/app/Console/Commands/Integrity/ReportEmptyObjects.php @@ -67,6 +67,51 @@ class ReportEmptyObjects extends Command return 0; } + /** + * Reports on accounts with no transactions. + */ + private function reportAccounts(): void + { + $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') + ->leftJoin('users', 'accounts.user_id', '=', 'users.id') + ->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']) + ->whereNull('transactions.account_id') + ->get( + ['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'] + ); + + /** @var stdClass $entry */ + foreach ($set as $entry) { + $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.'; + $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name); + $this->line($line); + } + } + + /** + * Reports on budgets with no budget limits (which makes them pointless). + */ + private function reportBudgetLimits(): void + { + $set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id') + ->leftJoin('users', 'budgets.user_id', '=', 'users.id') + ->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email']) + ->whereNull('budget_limits.id') + ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']); + + /** @var Budget $entry */ + foreach ($set as $entry) { + $line = sprintf( + 'User #%d (%s) has budget #%d ("%s") which has no budget limits.', + $entry->user_id, + $entry->email, + $entry->id, + $entry->name + ); + $this->line($line); + } + } + /** * Report on budgets with no transactions or journals. */ @@ -141,49 +186,4 @@ class ReportEmptyObjects extends Command $this->line($line); } } - - /** - * Reports on accounts with no transactions. - */ - private function reportAccounts(): void - { - $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') - ->leftJoin('users', 'accounts.user_id', '=', 'users.id') - ->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']) - ->whereNull('transactions.account_id') - ->get( - ['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'] - ); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.'; - $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name); - $this->line($line); - } - } - - /** - * Reports on budgets with no budget limits (which makes them pointless). - */ - private function reportBudgetLimits(): void - { - $set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id') - ->leftJoin('users', 'budgets.user_id', '=', 'users.id') - ->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email']) - ->whereNull('budget_limits.id') - ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']); - - /** @var Budget $entry */ - foreach ($set as $entry) { - $line = sprintf( - 'User #%d (%s) has budget #%d ("%s") which has no budget limits.', - $entry->user_id, - $entry->email, - $entry->id, - $entry->name - ); - $this->line($line); - } - } } diff --git a/app/Console/Commands/Integrity/ReportIntegrity.php b/app/Console/Commands/Integrity/ReportIntegrity.php index f6c21234f7..731ac0a9e1 100644 --- a/app/Console/Commands/Integrity/ReportIntegrity.php +++ b/app/Console/Commands/Integrity/ReportIntegrity.php @@ -61,7 +61,7 @@ class ReportIntegrity extends Command 'firefly-iii:report-empty-objects', 'firefly-iii:report-sum', 'firefly-iii:restore-oauth-keys', - 'firefly-iii:upgrade-group-information' + 'firefly-iii:upgrade-group-information', ]; foreach ($commands as $command) { $this->line(sprintf('Now executing %s', $command)); diff --git a/app/Console/Commands/Integrity/RestoreOAuthKeys.php b/app/Console/Commands/Integrity/RestoreOAuthKeys.php index e92d6b827a..bbee5e20b7 100644 --- a/app/Console/Commands/Integrity/RestoreOAuthKeys.php +++ b/app/Console/Commands/Integrity/RestoreOAuthKeys.php @@ -58,6 +58,38 @@ class RestoreOAuthKeys extends Command return 0; } + /** + * + */ + private function generateKeys(): void + { + OAuthKeys::generateKeys(); + } + + /** + * @return bool + */ + private function keysInDatabase(): bool + { + return OAuthKeys::keysInDatabase(); + } + + /** + * @return bool + */ + private function keysOnDrive(): bool + { + return OAuthKeys::hasKeyFiles(); + } + + /** + * + */ + private function restoreKeysFromDB(): bool + { + return OAuthKeys::restoreKeysFromDB(); + } + /** * */ @@ -97,30 +129,6 @@ class RestoreOAuthKeys extends Command $this->line('OAuth keys are OK'); } - /** - * @return bool - */ - private function keysInDatabase(): bool - { - return OAuthKeys::keysInDatabase(); - } - - /** - * @return bool - */ - private function keysOnDrive(): bool - { - return OAuthKeys::hasKeyFiles(); - } - - /** - * - */ - private function generateKeys(): void - { - OAuthKeys::generateKeys(); - } - /** * */ @@ -128,12 +136,4 @@ class RestoreOAuthKeys extends Command { OAuthKeys::storeKeysInDB(); } - - /** - * - */ - private function restoreKeysFromDB(): bool - { - return OAuthKeys::restoreKeysFromDB(); - } } diff --git a/app/Console/Commands/System/ForceDecimalSize.php b/app/Console/Commands/System/ForceDecimalSize.php index 66eefca3a5..3104baceb4 100644 --- a/app/Console/Commands/System/ForceDecimalSize.php +++ b/app/Console/Commands/System/ForceDecimalSize.php @@ -25,35 +25,62 @@ namespace FireflyIII\Console\Commands\System; use FireflyIII\Console\Commands\VerifiesAccessToken; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AutoBudget; +use FireflyIII\Models\AvailableBudget; +use FireflyIII\Models\Bill; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankEvent; +use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\RecurrenceTransaction; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +/** + * Class ForceDecimalSize + * + * This command was inspired by https://github.com/elliot-gh. It will check all amount fields + * and their values and correct them to the correct number of decimal places. This fixes issues where + * Firefly III would store 0.01 as 0.01000000000000000020816681711721685132943093776702880859375. + */ class ForceDecimalSize extends Command { use VerifiesAccessToken; - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'firefly-iii:force-decimal-size + + protected $description = 'This command resizes DECIMAL columns in MySQL or PostgreSQL and correct amounts (only MySQL).'; + protected $signature = 'firefly-iii:force-decimal-size {--user=1 : The user ID.} {--token= : The user\'s access token.}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'This command resizes DECIMAL columns in MySQL or PostgreSQL.'; + private array $decimals = []; + private array $tables = [ + 'accounts' => ['virtual_balance'], + 'auto_budgets' => ['amount'], + 'available_budgets' => ['amount'], + 'bills' => ['amount_min', 'amount_max'], + 'budget_limits' => ['amount'], + 'currency_exchange_rates' => ['rate', 'user_rate'], + 'limit_repetitions' => ['amount'], + 'piggy_bank_events' => ['amount'], + 'piggy_bank_repetitions' => ['currentamount'], + 'piggy_banks' => ['targetamount'], + 'recurrences_transactions' => ['amount', 'foreign_amount'], + 'transactions' => ['amount', 'foreign_amount'], + ]; /** * Execute the console command. + * * @throws FireflyException */ public function handle(): int { + Log::debug('Now in ForceDecimalSize::handle()'); if (!$this->verifyAccessToken()) { $this->error('Invalid access token.'); @@ -66,37 +93,560 @@ class ForceDecimalSize extends Command if (true === $question) { $user = $this->getUser(); Log::channel('audit')->info(sprintf('User #%d ("%s") forced DECIMAL size.', $user->id, $user->email)); + $this->correctAmounts(); $this->updateDecimals(); - return 0; } $this->line('Done!'); return 0; } - private function updateDecimals(): void + /** + * This method loops over all accounts and validates the amounts. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctAccountAmounts(TransactionCurrency $currency, array $fields): void { - $this->info('Going to force the size of DECIMAL columns. Please hold.'); - $tables = [ - 'accounts' => ['virtual_balance'], - 'auto_budgets' => ['amount'], - 'available_budgets' => ['amount'], - 'bills' => ['amount_min', 'amount_max'], - 'budget_limits' => ['amount'], - 'currency_exchange_rates' => ['rate', 'user_rate'], - 'limit_repetitions' => ['amount'], - 'piggy_bank_events' => ['amount'], - 'piggy_bank_repetitions' => ['currentamount'], - 'piggy_banks' => ['targetamount'], - 'recurrences_transactions' => ['amount', 'foreign_amount'], - 'transactions' => ['amount', 'foreign_amount'], - ]; + /** @var Builder $query */ + $query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->where('account_meta.name', 'currency_id') + ->where('account_meta.data', json_encode((string)$currency->id)); + $query->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(accounts.%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['accounts.*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All accounts in %s', $currency->code)); + return; + } + /** @var Account $account */ + foreach ($result as $account) { + foreach ($fields as $field) { + $value = $account->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct)); + Account::find($account->id)->update([$field => $correct]); + } + } + } + + /** + * This method checks if a basic check can be done or if it needs to be complicated. + * + * @return void + */ + private function correctAmounts(): void + { + if ('mysql' !== config('database.default')) { + $this->line('Skip correcting amounts...'); + return; + } + $this->correctAmountsByCurrency(); + } + + /** + * This method fixes all auto budgets in currency $currency. + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctAutoBudgetAmounts(TransactionCurrency $currency, array $fields): void + { + /** @var Builder $query */ + $query = AutoBudget::where('transaction_currency_id', $currency->id)->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All auto budgets in %s', $currency->code)); + return; + } + /** @var AutoBudget $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Auto budget #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + AutoBudget::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method loops all enabled currencies and then calls the method that will fix all objects in this currency. + * + * @return void + */ + private function correctAmountsByCurrency(): void + { + $this->line('Going to correct amounts, using the SLOW lane.'); + /** @var Collection $enabled */ + $enabled = TransactionCurrency::whereEnabled(1)->get(); + /** @var TransactionCurrency $currency */ + foreach ($enabled as $currency) { + $this->correctByCurrency($currency); + } + } + + /** + * This method fixes all available budgets in currency $currency. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctAvailableBudgetAmounts(TransactionCurrency $currency, array $fields): void + { + /** @var Builder $query */ + $query = AvailableBudget::where('transaction_currency_id', $currency->id)->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All available budgets in %s', $currency->code)); + return; + } + /** @var AvailableBudget $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Available budget #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + AvailableBudget::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all bills in currency $currency. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctBillAmounts(TransactionCurrency $currency, array $fields): void + { + /** @var Builder $query */ + $query = Bill::where('transaction_currency_id', $currency->id)->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All bills in %s', $currency->code)); + return; + } + /** @var Bill $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Bill #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + Bill::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all budget limits in currency $currency. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctBudgetLimitAmounts(TransactionCurrency $currency, array $fields) + { + /** @var Builder $query */ + $query = BudgetLimit::where('transaction_currency_id', $currency->id)->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All budget limits in %s', $currency->code)); + return; + } + /** @var BudgetLimit $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Budget limit #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + BudgetLimit::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method loops the available tables that may need fixing, and calls for the right method that can fix them. + * + * @param TransactionCurrency $currency + * @return void + * @throws FireflyException + */ + private function correctByCurrency(TransactionCurrency $currency): void + { + $this->line(sprintf('Going to correct amounts in currency %s ("%s").', $currency->code, $currency->name)); /** * @var string $name * @var array $fields */ - foreach($tables as $name => $fields) { + foreach ($this->tables as $name => $fields) { + switch ($name) { + default: + $message = sprintf('Cannot handle table "%s"', $name); + $this->line($message); + throw new FireflyException($message); + break; + case 'accounts': + $this->correctAccountAmounts($currency, $fields); + break; + case 'auto_budgets': + $this->correctAutoBudgetAmounts($currency, $fields); + break; + case 'available_budgets': + $this->correctAvailableBudgetAmounts($currency, $fields); + break; + case 'bills': + $this->correctBillAmounts($currency, $fields); + break; + case 'budget_limits': + $this->correctBudgetLimitAmounts($currency, $fields); + break; + case 'currency_exchange_rates': + case 'limit_repetitions': + // do nothing + break; + case 'piggy_bank_events': + $this->correctPiggyEventAmounts($currency, $fields); + break; + case 'piggy_bank_repetitions': + $this->correctPiggyRepetitionAmounts($currency, $fields); + break; + case 'piggy_banks': + $this->correctPiggyAmounts($currency, $fields); + break; + case 'recurrences_transactions': + $this->correctRecurringTransactionAmounts($currency, $fields); + break; + case 'transactions': + $this->correctTransactionAmounts($currency); + break; + } + } + } + + /** + * This method fixes all piggy banks in currency $currency. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctPiggyAmounts(TransactionCurrency $currency, array $fields) + { + /** @var Builder $query */ + $query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->where('account_meta.name', 'currency_id') + ->where('account_meta.data', json_encode((string)$currency->id)) + ->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(piggy_banks.%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['piggy_banks.*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All piggy banks in %s', $currency->code)); + return; + } + /** @var PiggyBank $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + PiggyBank::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all piggy bank events in currency $currency. + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctPiggyEventAmounts(TransactionCurrency $currency, array $fields): void + { + /** @var Builder $query */ + $query = PiggyBankEvent::leftJoin('piggy_banks', 'piggy_bank_events.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->where('account_meta.name', 'currency_id') + ->where('account_meta.data', json_encode((string)$currency->id)) + ->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(piggy_bank_events.%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['piggy_bank_events.*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All piggy bank events in %s', $currency->code)); + return; + } + /** @var PiggyBankEvent $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Piggy bank event #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + PiggyBankEvent::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all piggy bank repetitions in currency $currency. + * + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctPiggyRepetitionAmounts(TransactionCurrency $currency, array $fields) + { + // select all piggy bank repetitions with this currency and issue. + /** @var Builder $query */ + $query = PiggyBankRepetition::leftJoin('piggy_banks', 'piggy_bank_repetitions.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->where('account_meta.name', 'currency_id') + ->where('account_meta.data', json_encode((string)$currency->id)) + ->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(piggy_bank_repetitions.%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['piggy_bank_repetitions.*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All piggy bank repetitions in %s', $currency->code)); + return; + } + /** @var PiggyBankRepetition $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Piggy bank repetition #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + PiggyBankRepetition::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all recurring transactions in currency $currency. + * @param TransactionCurrency $currency + * @param array $fields + * @return void + */ + private function correctRecurringTransactionAmounts(TransactionCurrency $currency, array $fields): void + { + /** @var Builder $query */ + $query = RecurrenceTransaction::where('transaction_currency_id', $currency->id)->where(static function (Builder $q) use ($fields, $currency) { + foreach ($fields as $field) { + $q->orWhere( + DB::raw(sprintf('CAST(%s AS CHAR)', $field)), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + } + }); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All recurring transactions in %s', $currency->code)); + return; + } + /** @var RecurrenceTransaction $item */ + foreach ($result as $item) { + foreach ($fields as $field) { + $value = $item->$field; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Recurring transaction #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); + RecurrenceTransaction::find($item->id)->update([$field => $correct]); + } + } + } + + /** + * This method fixes all transactions in currency $currency. + * + * @param TransactionCurrency $currency + * @return void + */ + private function correctTransactionAmounts(TransactionCurrency $currency): void + { + // select all transactions with this currency and issue. + /** @var Builder $query */ + $query = Transaction::where('transaction_currency_id', $currency->id)->where( + DB::raw('CAST(amount AS CHAR)'), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All transactions in %s', $currency->code)); + } + /** @var Transaction $item */ + foreach ($result as $item) { + $value = $item->amount; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Transaction #%d has amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct)); + Transaction::find($item->id)->update(['amount' => $correct]); + } + + // select all transactions with this FOREIGN currency and issue. + /** @var Builder $query */ + $query = Transaction::where('foreign_currency_id', $currency->id)->where( + DB::raw('CAST(foreign_amount AS CHAR)'), + 'REGEXP', + DB::raw(sprintf('\'\\\\.[\\\\d]{%d}[1-9]+\'', $currency->decimal_places)) + ); + + $result = $query->get(['*']); + if (0 === $result->count()) { + $this->line(sprintf('Correct: All transactions in foreign currency %s', $currency->code)); + return; + } + /** @var Transaction $item */ + foreach ($result as $item) { + $value = $item->foreign_amount; + if (null === $value) { + continue; + } + // fix $field by rounding it down correctly. + $pow = pow(10, (int)$currency->decimal_places); + $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $this->line(sprintf('Transaction #%d has foreign amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct)); + Transaction::find($item->id)->update(['foreign_amount' => $correct]); + } + } + + /** + * @return void + */ + private function updateDecimals(): void + { + $this->info('Going to force the size of DECIMAL columns. Please hold.'); + + /** + * @var string $name + * @var array $fields + */ + foreach ($this->tables as $name => $fields) { /** @var string $field */ - foreach($fields as $field) { + foreach ($fields as $field) { $this->line(sprintf('Updating table "%s", field "%s"...', $name, $field)); $query = sprintf('ALTER TABLE %s CHANGE COLUMN %s %s DECIMAL(32, 12);', $name, $field, $field); DB::select($query); diff --git a/app/Console/Commands/System/ForceMigration.php b/app/Console/Commands/System/ForceMigration.php index 43c96ebe7a..dc28c322c0 100644 --- a/app/Console/Commands/System/ForceMigration.php +++ b/app/Console/Commands/System/ForceMigration.php @@ -35,6 +35,12 @@ class ForceMigration extends Command { use VerifiesAccessToken; + /** + * The console command description. + * + * @var string + */ + protected $description = 'This command will force-run all database migrations.'; /** * The name and signature of the console command. * @@ -44,13 +50,6 @@ class ForceMigration extends Command {--user=1 : The user ID.} {--token= : The user\'s access token.}'; - /** - * The console command description. - * - * @var string - */ - protected $description = 'This command will force-run all database migrations.'; - /** * Execute the console command. * @throws FireflyException diff --git a/app/Console/Commands/System/UpgradeFireflyInstructions.php b/app/Console/Commands/System/UpgradeFireflyInstructions.php index e0ab1837c7..1e0330b0dc 100644 --- a/app/Console/Commands/System/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/System/UpgradeFireflyInstructions.php @@ -66,52 +66,6 @@ class UpgradeFireflyInstructions extends Command return 0; } - /** - * Render upgrade instructions. - */ - private function updateInstructions(): void - { - /** @var string $version */ - $version = config('firefly.version'); - $config = config('upgrade.text.upgrade'); - $text = ''; - foreach (array_keys($config) as $compare) { - // if string starts with: - if (\str_starts_with($version, $compare)) { - $text = $config[$compare]; - } - - } - - $this->showLine(); - $this->boxed(''); - if (null === $text || '' === $text) { - $this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version)); - $this->boxedInfo('There are no extra upgrade instructions.'); - $this->boxed('Firefly III should be ready for use.'); - $this->boxed(''); - $this->showLine(); - - return; - } - - $this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version)); - $this->boxedInfo($text); - $this->boxed(''); - $this->showLine(); - } - - /** - * Show a line. - */ - private function showLine(): void - { - $line = '+'; - $line .= str_repeat('-', 78); - $line .= '+'; - $this->line($line); - } - /** * Show a nice box. * @@ -170,4 +124,49 @@ class UpgradeFireflyInstructions extends Command $this->boxed(''); $this->showLine(); } + + /** + * Show a line. + */ + private function showLine(): void + { + $line = '+'; + $line .= str_repeat('-', 78); + $line .= '+'; + $this->line($line); + } + + /** + * Render upgrade instructions. + */ + private function updateInstructions(): void + { + /** @var string $version */ + $version = config('firefly.version'); + $config = config('upgrade.text.upgrade'); + $text = ''; + foreach (array_keys($config) as $compare) { + // if string starts with: + if (\str_starts_with($version, $compare)) { + $text = $config[$compare]; + } + } + + $this->showLine(); + $this->boxed(''); + if (null === $text || '' === $text) { + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version)); + $this->boxedInfo('There are no extra upgrade instructions.'); + $this->boxed('Firefly III should be ready for use.'); + $this->boxed(''); + $this->showLine(); + + return; + } + + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version)); + $this->boxedInfo($text); + $this->boxed(''); + $this->showLine(); + } } diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 83c00a8f59..83c9d1f222 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -31,8 +31,8 @@ use FireflyIII\Support\Cronjobs\BillWarningCronjob; use FireflyIII\Support\Cronjobs\ExchangeRatesCronjob; use FireflyIII\Support\Cronjobs\RecurringCronjob; use Illuminate\Console\Command; -use InvalidArgumentException; use Illuminate\Support\Facades\Log; +use InvalidArgumentException; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; @@ -123,62 +123,6 @@ class Cron extends Command return 0; } - /** - * @param bool $force - * @param Carbon|null $date - */ - private function exchangeRatesCronJob(bool $force, ?Carbon $date): void - { - $exchangeRates = new ExchangeRatesCronjob(); - $exchangeRates->setForce($force); - // set date in cron job: - if (null !== $date) { - $exchangeRates->setDate($date); - } - - $exchangeRates->fire(); - - if ($exchangeRates->jobErrored) { - $this->error(sprintf('Error in "exchange rates" cron: %s', $exchangeRates->message)); - } - if ($exchangeRates->jobFired) { - $this->line(sprintf('"Exchange rates" cron fired: %s', $exchangeRates->message)); - } - if ($exchangeRates->jobSucceeded) { - $this->info(sprintf('"Exchange rates" cron ran with success: %s', $exchangeRates->message)); - } - } - - /** - * @param bool $force - * @param Carbon|null $date - * - * @throws ContainerExceptionInterface - * @throws FireflyException - * @throws NotFoundExceptionInterface - */ - private function recurringCronJob(bool $force, ?Carbon $date): void - { - $recurring = new RecurringCronjob(); - $recurring->setForce($force); - - // set date in cron job: - if (null !== $date) { - $recurring->setDate($date); - } - - $recurring->fire(); - if ($recurring->jobErrored) { - $this->error(sprintf('Error in "create recurring transactions" cron: %s', $recurring->message)); - } - if ($recurring->jobFired) { - $this->line(sprintf('"Create recurring transactions" cron fired: %s', $recurring->message)); - } - if ($recurring->jobSucceeded) { - $this->info(sprintf('"Create recurring transactions" cron ran with success: %s', $recurring->message)); - } - } - /** * @param bool $force * @param Carbon|null $date @@ -234,4 +178,60 @@ class Cron extends Command $this->info(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message)); } } + + /** + * @param bool $force + * @param Carbon|null $date + */ + private function exchangeRatesCronJob(bool $force, ?Carbon $date): void + { + $exchangeRates = new ExchangeRatesCronjob(); + $exchangeRates->setForce($force); + // set date in cron job: + if (null !== $date) { + $exchangeRates->setDate($date); + } + + $exchangeRates->fire(); + + if ($exchangeRates->jobErrored) { + $this->error(sprintf('Error in "exchange rates" cron: %s', $exchangeRates->message)); + } + if ($exchangeRates->jobFired) { + $this->line(sprintf('"Exchange rates" cron fired: %s', $exchangeRates->message)); + } + if ($exchangeRates->jobSucceeded) { + $this->info(sprintf('"Exchange rates" cron ran with success: %s', $exchangeRates->message)); + } + } + + /** + * @param bool $force + * @param Carbon|null $date + * + * @throws ContainerExceptionInterface + * @throws FireflyException + * @throws NotFoundExceptionInterface + */ + private function recurringCronJob(bool $force, ?Carbon $date): void + { + $recurring = new RecurringCronjob(); + $recurring->setForce($force); + + // set date in cron job: + if (null !== $date) { + $recurring->setDate($date); + } + + $recurring->fire(); + if ($recurring->jobErrored) { + $this->error(sprintf('Error in "create recurring transactions" cron: %s', $recurring->message)); + } + if ($recurring->jobFired) { + $this->line(sprintf('"Create recurring transactions" cron fired: %s', $recurring->message)); + } + if ($recurring->jobSucceeded) { + $this->info(sprintf('"Create recurring transactions" cron ran with success: %s', $recurring->message)); + } + } } diff --git a/app/Console/Commands/Upgrade/AccountCurrencies.php b/app/Console/Commands/Upgrade/AccountCurrencies.php index e53f934603..276e3e7668 100644 --- a/app/Console/Commands/Upgrade/AccountCurrencies.php +++ b/app/Console/Commands/Upgrade/AccountCurrencies.php @@ -96,20 +96,6 @@ class AccountCurrencies extends Command return 0; } - /** - * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is - * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should - * be called from the handle method instead of using the constructor to initialize the command. - * - - */ - private function stupidLaravel(): void - { - $this->accountRepos = app(AccountRepositoryInterface::class); - $this->userRepos = app(UserRepositoryInterface::class); - $this->count = 0; - } - /** * @return bool * @throws ContainerExceptionInterface @@ -128,50 +114,23 @@ class AccountCurrencies extends Command /** * */ - private function updateAccountCurrencies(): void + private function markAsExecuted(): void { - Log::debug('Now in updateAccountCurrencies()'); - $users = $this->userRepos->all(); - $defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR'); - Log::debug(sprintf('Default currency is %s', $defaultCurrencyCode)); - foreach ($users as $user) { - $this->updateCurrenciesForUser($user, $defaultCurrencyCode); - } + app('fireflyconfig')->set(self::CONFIG_NAME, true); } /** - * @param User $user - * @param string $systemCurrencyCode + * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is + * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should + * be called from the handle method instead of using the constructor to initialize the command. * - * @throws FireflyException + */ - private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void + private function stupidLaravel(): void { - Log::debug(sprintf('Now in updateCurrenciesForUser(%s, %s)', $user->email, $systemCurrencyCode)); - $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; - if (!is_string($defaultCurrencyCode)) { - $defaultCurrencyCode = $systemCurrencyCode; - } - Log::debug(sprintf('Users currency pref is %s', $defaultCurrencyCode)); - - /** @var TransactionCurrency|null $defaultCurrency */ - $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); - - if (null === $defaultCurrency) { - Log::error(sprintf('Users currency pref "%s" does not exist!', $defaultCurrencyCode)); - $this->error(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode)); - - return; - } - - /** @var Account $account */ - foreach ($accounts as $account) { - $this->updateAccount($account, $defaultCurrency); - } + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->userRepos = app(UserRepositoryInterface::class); + $this->count = 0; } /** @@ -237,8 +196,49 @@ class AccountCurrencies extends Command /** * */ - private function markAsExecuted(): void + private function updateAccountCurrencies(): void { - app('fireflyconfig')->set(self::CONFIG_NAME, true); + Log::debug('Now in updateAccountCurrencies()'); + $users = $this->userRepos->all(); + $defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR'); + Log::debug(sprintf('Default currency is %s', $defaultCurrencyCode)); + foreach ($users as $user) { + $this->updateCurrenciesForUser($user, $defaultCurrencyCode); + } + } + + /** + * @param User $user + * @param string $systemCurrencyCode + * + * @throws FireflyException + */ + private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void + { + Log::debug(sprintf('Now in updateCurrenciesForUser(%s, %s)', $user->email, $systemCurrencyCode)); + $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; + if (!is_string($defaultCurrencyCode)) { + $defaultCurrencyCode = $systemCurrencyCode; + } + Log::debug(sprintf('Users currency pref is %s', $defaultCurrencyCode)); + + /** @var TransactionCurrency|null $defaultCurrency */ + $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); + + if (null === $defaultCurrency) { + Log::error(sprintf('Users currency pref "%s" does not exist!', $defaultCurrencyCode)); + $this->error(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode)); + + return; + } + + /** @var Account $account */ + foreach ($accounts as $account) { + $this->updateAccount($account, $defaultCurrency); + } } } diff --git a/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php b/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php index 54aa905772..a468b6f9e9 100644 --- a/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php +++ b/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php @@ -73,30 +73,6 @@ class AppendBudgetLimitPeriods extends Command return 0; } - /** - * @return bool - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - private function isExecuted(): bool - { - $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - - return (bool)$configVar->data; - } - - /** - * - */ - private function theresNoLimit(): void - { - $limits = BudgetLimit::whereNull('period')->get(); - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $this->fixLimit($limit); - } - } - /** * @param BudgetLimit $limit */ @@ -182,6 +158,18 @@ class AppendBudgetLimitPeriods extends Command return null; } + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + + return (bool)$configVar->data; + } + /** * */ @@ -189,4 +177,16 @@ class AppendBudgetLimitPeriods extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } + + /** + * + */ + private function theresNoLimit(): void + { + $limits = BudgetLimit::whereNull('period')->get(); + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $this->fixLimit($limit); + } + } } diff --git a/app/Console/Commands/Upgrade/BackToJournals.php b/app/Console/Commands/Upgrade/BackToJournals.php index 537a1d3966..cf3ddadad9 100644 --- a/app/Console/Commands/Upgrade/BackToJournals.php +++ b/app/Console/Commands/Upgrade/BackToJournals.php @@ -87,15 +87,39 @@ class BackToJournals extends Command } /** - * @return bool - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface + * @return array */ - private function isMigrated(): bool + private function getIdsForBudgets(): array { - $configVar = app('fireflyconfig')->get(MigrateToGroups::CONFIG_NAME, false); + $transactions = DB::table('budget_transaction')->distinct()->pluck('transaction_id')->toArray(); + $array = []; + $chunks = array_chunk($transactions, 500); - return (bool)$configVar->data; + foreach ($chunks as $chunk) { + $set = DB::table('transactions')->whereIn('transactions.id', $chunk)->pluck('transaction_journal_id')->toArray(); + $array = array_merge($array, $set); + } + + return $array; + } + + /** + * @return array + */ + private function getIdsForCategories(): array + { + $transactions = DB::table('category_transaction')->distinct()->pluck('transaction_id')->toArray(); + $array = []; + $chunks = array_chunk($transactions, 500); + + foreach ($chunks as $chunk) { + $set = DB::table('transactions') + ->whereIn('transactions.id', $chunk) + ->pluck('transaction_journal_id')->toArray(); + $array = array_merge($array, $set); + } + + return $array; } /** @@ -110,6 +134,26 @@ class BackToJournals extends Command return (bool)$configVar->data; } + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isMigrated(): bool + { + $configVar = app('fireflyconfig')->get(MigrateToGroups::CONFIG_NAME, false); + + return (bool)$configVar->data; + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + /** * */ @@ -143,23 +187,6 @@ class BackToJournals extends Command } } - /** - * @return array - */ - private function getIdsForBudgets(): array - { - $transactions = DB::table('budget_transaction')->distinct()->pluck('transaction_id')->toArray(); - $array = []; - $chunks = array_chunk($transactions, 500); - - foreach ($chunks as $chunk) { - $set = DB::table('transactions')->whereIn('transactions.id', $chunk)->pluck('transaction_journal_id')->toArray(); - $array = array_merge($array, $set); - } - - return $array; - } - /** * @param TransactionJournal $journal */ @@ -217,25 +244,6 @@ class BackToJournals extends Command } } - /** - * @return array - */ - private function getIdsForCategories(): array - { - $transactions = DB::table('category_transaction')->distinct()->pluck('transaction_id')->toArray(); - $array = []; - $chunks = array_chunk($transactions, 500); - - foreach ($chunks as $chunk) { - $set = DB::table('transactions') - ->whereIn('transactions.id', $chunk) - ->pluck('transaction_journal_id')->toArray(); - $array = array_merge($array, $set); - } - - return $array; - } - /** * @param TransactionJournal $journal */ @@ -265,12 +273,4 @@ class BackToJournals extends Command $journal->categories()->sync([(int)$category->id]); } } - - /** - * - */ - private function markAsExecuted(): void - { - app('fireflyconfig')->set(self::CONFIG_NAME, true); - } } diff --git a/app/Console/Commands/Upgrade/DecryptDatabase.php b/app/Console/Commands/Upgrade/DecryptDatabase.php index 45051dc6f4..f7fba6b891 100644 --- a/app/Console/Commands/Upgrade/DecryptDatabase.php +++ b/app/Console/Commands/Upgrade/DecryptDatabase.php @@ -87,6 +87,81 @@ class DecryptDatabase extends Command return 0; } + /** + * @param string $table + * @param string $field + */ + private function decryptField(string $table, string $field): void + { + $rows = DB::table($table)->get(['id', $field]); + /** @var stdClass $row */ + foreach ($rows as $row) { + $this->decryptRow($table, $field, $row); + } + } + + /** + * @param int $id + * @param string $value + */ + private function decryptPreferencesRow(int $id, string $value): void + { + // try to json_decrypt the value. + try { + $newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value; + } catch (JsonException $e) { + $message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage()); + $this->error($message); + app('log')->warning($message); + app('log')->warning($value); + app('log')->warning($e->getTraceAsString()); + + return; + } + + /** @var Preference $object */ + $object = Preference::find((int)$id); + if (null !== $object) { + $object->data = $newValue; + $object->save(); + } + } + + /** + * @param string $table + * @param string $field + * @param stdClass $row + */ + private function decryptRow(string $table, string $field, stdClass $row): void + { + $original = $row->$field; + if (null === $original) { + return; + } + $id = (int)$row->id; + $value = ''; + + try { + $value = $this->tryDecrypt($original); + } catch (FireflyException $e) { + $message = sprintf('Could not decrypt field "%s" in row #%d of table "%s": %s', $field, $id, $table, $e->getMessage()); + $this->error($message); + Log::error($message); + Log::error($e->getTraceAsString()); + } + + // A separate routine for preferences table: + if ('preferences' === $table) { + $this->decryptPreferencesRow($id, $value); + + return; + } + + if ($value !== $original) { + DB::table($table)->where('id', $id)->update([$field => $value]); + } + } + /** * @param string $table * @param array $fields @@ -132,54 +207,6 @@ class DecryptDatabase extends Command return false; } - /** - * @param string $table - * @param string $field - */ - private function decryptField(string $table, string $field): void - { - $rows = DB::table($table)->get(['id', $field]); - /** @var stdClass $row */ - foreach ($rows as $row) { - $this->decryptRow($table, $field, $row); - } - } - - /** - * @param string $table - * @param string $field - * @param stdClass $row - */ - private function decryptRow(string $table, string $field, stdClass $row): void - { - $original = $row->$field; - if (null === $original) { - return; - } - $id = (int)$row->id; - $value = ''; - - try { - $value = $this->tryDecrypt($original); - } catch (FireflyException $e) { - $message = sprintf('Could not decrypt field "%s" in row #%d of table "%s": %s', $field, $id, $table, $e->getMessage()); - $this->error($message); - Log::error($message); - Log::error($e->getTraceAsString()); - } - - // A separate routine for preferences table: - if ('preferences' === $table) { - $this->decryptPreferencesRow($id, $value); - - return; - } - - if ($value !== $original) { - DB::table($table)->where('id', $id)->update([$field => $value]); - } - } - /** * Tries to decrypt data. Will only throw an exception when the MAC is invalid. * @@ -200,31 +227,4 @@ class DecryptDatabase extends Command return $value; } - - /** - * @param int $id - * @param string $value - */ - private function decryptPreferencesRow(int $id, string $value): void - { - // try to json_decrypt the value. - try { - $newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value; - } catch (JsonException $e) { - $message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage()); - $this->error($message); - app('log')->warning($message); - app('log')->warning($value); - app('log')->warning($e->getTraceAsString()); - - return; - } - - /** @var Preference $object */ - $object = Preference::find((int)$id); - if (null !== $object) { - $object->data = $newValue; - $object->save(); - } - } } diff --git a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php b/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php index 14a01764c5..da09efbfcb 100644 --- a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php +++ b/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php @@ -102,20 +102,11 @@ class MigrateRecurrenceMeta extends Command } /** - * @return int - * @throws JsonException + * */ - private function migrateMetaData(): int + private function markAsExecuted(): void { - $count = 0; - // get all recurrence meta data: - $collection = RecurrenceMeta::with('recurrence')->get(); - /** @var RecurrenceMeta $meta */ - foreach ($collection as $meta) { - $count += $this->migrateEntry($meta); - } - - return $count; + app('fireflyconfig')->set(self::CONFIG_NAME, true); } /** @@ -155,10 +146,19 @@ class MigrateRecurrenceMeta extends Command } /** - * + * @return int + * @throws JsonException */ - private function markAsExecuted(): void + private function migrateMetaData(): int { - app('fireflyconfig')->set(self::CONFIG_NAME, true); + $count = 0; + // get all recurrence meta data: + $collection = RecurrenceMeta::with('recurrence')->get(); + /** @var RecurrenceMeta $meta */ + foreach ($collection as $meta) { + $count += $this->migrateEntry($meta); + } + + return $count; } } diff --git a/app/Console/Commands/Upgrade/MigrateRecurrenceType.php b/app/Console/Commands/Upgrade/MigrateRecurrenceType.php index edc2f67b08..fadff18491 100644 --- a/app/Console/Commands/Upgrade/MigrateRecurrenceType.php +++ b/app/Console/Commands/Upgrade/MigrateRecurrenceType.php @@ -78,6 +78,14 @@ class MigrateRecurrenceType extends Command return 0; } + /** + * + */ + private function getInvalidType(): TransactionType + { + return TransactionType::whereType(TransactionType::INVALID)->firstOrCreate(['type' => TransactionType::INVALID]); + } + /** * @return bool * @throws ContainerExceptionInterface @@ -96,15 +104,9 @@ class MigrateRecurrenceType extends Command /** * */ - private function migrateTypes(): void + private function markAsExecuted(): void { - $set = Recurrence::get(); - /** @var Recurrence $recurrence */ - foreach ($set as $recurrence) { - if ($recurrence->transactionType->type !== TransactionType::INVALID) { - $this->migrateRecurrence($recurrence); - } - } + app('fireflyconfig')->set(self::CONFIG_NAME, true); } private function migrateRecurrence(Recurrence $recurrence): void @@ -124,16 +126,14 @@ class MigrateRecurrenceType extends Command /** * */ - private function getInvalidType(): TransactionType + private function migrateTypes(): void { - return TransactionType::whereType(TransactionType::INVALID)->firstOrCreate(['type' => TransactionType::INVALID]); - } - - /** - * - */ - private function markAsExecuted(): void - { - app('fireflyconfig')->set(self::CONFIG_NAME, true); + $set = Recurrence::get(); + /** @var Recurrence $recurrence */ + foreach ($set as $recurrence) { + if ($recurrence->transactionType->type !== TransactionType::INVALID) { + $this->migrateRecurrence($recurrence); + } + } } } diff --git a/app/Console/Commands/Upgrade/MigrateTagLocations.php b/app/Console/Commands/Upgrade/MigrateTagLocations.php index 15c9f1b654..8e19076565 100644 --- a/app/Console/Commands/Upgrade/MigrateTagLocations.php +++ b/app/Console/Commands/Upgrade/MigrateTagLocations.php @@ -75,6 +75,16 @@ class MigrateTagLocations extends Command return 0; } + /** + * @param Tag $tag + * + * @return bool + */ + private function hasLocationDetails(Tag $tag): bool + { + return null !== $tag->latitude && null !== $tag->longitude && null !== $tag->zoomLevel; + } + /** * @return bool * @throws ContainerExceptionInterface @@ -90,25 +100,12 @@ class MigrateTagLocations extends Command return false; } - private function migrateTagLocations(): void - { - $tags = Tag::get(); - /** @var Tag $tag */ - foreach ($tags as $tag) { - if ($this->hasLocationDetails($tag)) { - $this->migrateLocationDetails($tag); - } - } - } - /** - * @param Tag $tag * - * @return bool */ - private function hasLocationDetails(Tag $tag): bool + private function markAsExecuted(): void { - return null !== $tag->latitude && null !== $tag->longitude && null !== $tag->zoomLevel; + app('fireflyconfig')->set(self::CONFIG_NAME, true); } /** @@ -129,11 +126,14 @@ class MigrateTagLocations extends Command $tag->save(); } - /** - * - */ - private function markAsExecuted(): void + private function migrateTagLocations(): void { - app('fireflyconfig')->set(self::CONFIG_NAME, true); + $tags = Tag::get(); + /** @var Tag $tag */ + foreach ($tags as $tag) { + if ($this->hasLocationDetails($tag)) { + $this->migrateLocationDetails($tag); + } + } } } diff --git a/app/Console/Commands/Upgrade/MigrateToRules.php b/app/Console/Commands/Upgrade/MigrateToRules.php index 802c6c849b..79a28c0d03 100644 --- a/app/Console/Commands/Upgrade/MigrateToRules.php +++ b/app/Console/Commands/Upgrade/MigrateToRules.php @@ -105,22 +105,6 @@ class MigrateToRules extends Command return 0; } - /** - * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is - * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should - * be called from the handle method instead of using the constructor to initialize the command. - * - - */ - private function stupidLaravel(): void - { - $this->count = 0; - $this->userRepository = app(UserRepositoryInterface::class); - $this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class); - $this->billRepository = app(BillRepositoryInterface::class); - $this->ruleRepository = app(RuleRepositoryInterface::class); - } - /** * @return bool * @throws ContainerExceptionInterface @@ -137,38 +121,11 @@ class MigrateToRules extends Command } /** - * Migrate bills to new rule structure for a specific user. * - * @param User $user - * - * @throws FireflyException */ - private function migrateUser(User $user): void + private function markAsExecuted(): void { - $this->ruleGroupRepository->setUser($user); - $this->billRepository->setUser($user); - $this->ruleRepository->setUser($user); - - /** @var Preference $lang */ - $lang = app('preferences')->getForUser($user, 'language', 'en_US'); - $groupTitle = (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data); - $ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle); - - if (null === $ruleGroup) { - $ruleGroup = $this->ruleGroupRepository->store( - [ - 'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data), - 'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $lang->data), - 'active' => true, - ] - ); - } - $bills = $this->billRepository->getBills(); - - /** @var Bill $bill */ - foreach ($bills as $bill) { - $this->migrateBill($ruleGroup, $bill, $lang); - } + app('fireflyconfig')->set(self::CONFIG_NAME, true); } /** @@ -243,10 +200,53 @@ class MigrateToRules extends Command } /** + * Migrate bills to new rule structure for a specific user. * + * @param User $user + * + * @throws FireflyException */ - private function markAsExecuted(): void + private function migrateUser(User $user): void { - app('fireflyconfig')->set(self::CONFIG_NAME, true); + $this->ruleGroupRepository->setUser($user); + $this->billRepository->setUser($user); + $this->ruleRepository->setUser($user); + + /** @var Preference $lang */ + $lang = app('preferences')->getForUser($user, 'language', 'en_US'); + $groupTitle = (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data); + $ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle); + + if (null === $ruleGroup) { + $ruleGroup = $this->ruleGroupRepository->store( + [ + 'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data), + 'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $lang->data), + 'active' => true, + ] + ); + } + $bills = $this->billRepository->getBills(); + + /** @var Bill $bill */ + foreach ($bills as $bill) { + $this->migrateBill($ruleGroup, $bill, $lang); + } + } + + /** + * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is + * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should + * be called from the handle method instead of using the constructor to initialize the command. + * + + */ + private function stupidLaravel(): void + { + $this->count = 0; + $this->userRepository = app(UserRepositoryInterface::class); + $this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class); + $this->billRepository = app(BillRepositoryInterface::class); + $this->ruleRepository = app(RuleRepositoryInterface::class); } } diff --git a/app/Console/Commands/Upgrade/TransactionIdentifier.php b/app/Console/Commands/Upgrade/TransactionIdentifier.php index 6500ec8170..8eca9098f3 100644 --- a/app/Console/Commands/Upgrade/TransactionIdentifier.php +++ b/app/Console/Commands/Upgrade/TransactionIdentifier.php @@ -107,63 +107,6 @@ class TransactionIdentifier extends Command return 0; } - /** - * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is - * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should - * be called from the handle method instead of using the constructor to initialize the command. - * - - */ - private function stupidLaravel(): void - { - $this->cliRepository = app(JournalCLIRepositoryInterface::class); - $this->count = 0; - } - - /** - * @return bool - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - private function isExecuted(): bool - { - $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - if (null !== $configVar) { - return (bool)$configVar->data; - } - - return false; - } - - /** - * Grab all positive transactions from this journal that are not deleted. for each one, grab the negative opposing one - * which has 0 as an identifier and give it the same identifier. - * - * @param TransactionJournal $transactionJournal - */ - private function updateJournalIdentifiers(TransactionJournal $transactionJournal): void - { - $identifier = 0; - $exclude = []; // transactions already processed. - $transactions = $transactionJournal->transactions()->where('amount', '>', 0)->get(); - - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - $opposing = $this->findOpposing($transaction, $exclude); - if (null !== $opposing) { - // give both a new identifier: - $transaction->identifier = $identifier; - $opposing->identifier = $identifier; - $transaction->save(); - $opposing->save(); - $exclude[] = $transaction->id; - $exclude[] = $opposing->id; - $this->count++; - } - ++$identifier; - } - } - /** * @param Transaction $transaction * @param array $exclude @@ -194,6 +137,21 @@ class TransactionIdentifier extends Command return $opposing; } + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + /** * */ @@ -201,4 +159,46 @@ class TransactionIdentifier extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } + + /** + * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is + * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should + * be called from the handle method instead of using the constructor to initialize the command. + * + + */ + private function stupidLaravel(): void + { + $this->cliRepository = app(JournalCLIRepositoryInterface::class); + $this->count = 0; + } + + /** + * Grab all positive transactions from this journal that are not deleted. for each one, grab the negative opposing one + * which has 0 as an identifier and give it the same identifier. + * + * @param TransactionJournal $transactionJournal + */ + private function updateJournalIdentifiers(TransactionJournal $transactionJournal): void + { + $identifier = 0; + $exclude = []; // transactions already processed. + $transactions = $transactionJournal->transactions()->where('amount', '>', 0)->get(); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $opposing = $this->findOpposing($transaction, $exclude); + if (null !== $opposing) { + // give both a new identifier: + $transaction->identifier = $identifier; + $opposing->identifier = $identifier; + $transaction->save(); + $opposing->save(); + $exclude[] = $transaction->id; + $exclude[] = $opposing->id; + $this->count++; + } + ++$identifier; + } + } } diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradeDatabase.php index 492d4ec069..09ea26f05e 100644 --- a/app/Console/Commands/Upgrade/UpgradeDatabase.php +++ b/app/Console/Commands/Upgrade/UpgradeDatabase.php @@ -98,7 +98,7 @@ class UpgradeDatabase extends Command private function callInitialCommands(): void { $this->line('Now seeding the database...'); - $this->call('migrate', ['--seed' => true, '--force' => true,'--no-interaction' => true]); + $this->call('migrate', ['--seed' => true, '--force' => true, '--no-interaction' => true]); $this->line('Fix PostgreSQL sequences.'); $this->call('firefly-iii:fix-pgsql-sequences'); diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradeLiabilities.php index 2890298b92..7e93c7f44d 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradeLiabilities.php @@ -82,79 +82,6 @@ class UpgradeLiabilities extends Command return 0; } - /** - * @return bool - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - private function isExecuted(): bool - { - $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - if (null !== $configVar) { - return (bool)$configVar->data; - } - - return false; - } - - /** - * - */ - private function upgradeLiabilities(): void - { - Log::debug('Upgrading liabilities.'); - $users = User::get(); - /** @var User $user */ - foreach ($users as $user) { - $this->upgradeForUser($user); - } - } - - /** - * @param User $user - */ - private function upgradeForUser(User $user): void - { - Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); - $accounts = $user->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', config('firefly.valid_liabilities')) - ->get(['accounts.*']); - /** @var Account $account */ - foreach ($accounts as $account) { - $this->upgradeLiability($account); - $service = app(CreditRecalculateService::class); - $service->setAccount($account); - $service->recalculate(); - } - } - - /** - * @param Account $account - */ - private function upgradeLiability(Account $account): void - { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($account->user); - Log::debug(sprintf('Upgrade liability #%d', $account->id)); - - // get opening balance, and correct if necessary. - $openingBalance = $repository->getOpeningBalance($account); - if (null !== $openingBalance) { - // correct if necessary - $this->correctOpeningBalance($account, $openingBalance); - } - - // add liability direction property (if it does not yet exist!) - $value = $repository->getMetaValue($account, 'liability_direction'); - if (null === $value) { - /** @var AccountMetaFactory $factory */ - $factory = app(AccountMetaFactory::class); - $factory->crud($account, 'liability_direction', 'debit'); - } - } - /** * @param Account $account * @param TransactionJournal $openingBalance @@ -185,9 +112,9 @@ class UpgradeLiabilities extends Command * * @return Transaction|null */ - private function getSourceTransaction(TransactionJournal $journal): ?Transaction + private function getDestinationTransaction(TransactionJournal $journal): ?Transaction { - return $journal->transactions()->where('amount', '<', 0)->first(); + return $journal->transactions()->where('amount', '>', 0)->first(); } /** @@ -195,9 +122,24 @@ class UpgradeLiabilities extends Command * * @return Transaction|null */ - private function getDestinationTransaction(TransactionJournal $journal): ?Transaction + private function getSourceTransaction(TransactionJournal $journal): ?Transaction { - return $journal->transactions()->where('amount', '>', 0)->first(); + return $journal->transactions()->where('amount', '<', 0)->first(); + } + + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; } /** @@ -207,4 +149,62 @@ class UpgradeLiabilities extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } + + /** + * @param User $user + */ + private function upgradeForUser(User $user): void + { + Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); + $accounts = $user->accounts() + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + $this->upgradeLiability($account); + $service = app(CreditRecalculateService::class); + $service->setAccount($account); + $service->recalculate(); + } + } + + /** + * + */ + private function upgradeLiabilities(): void + { + Log::debug('Upgrading liabilities.'); + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + $this->upgradeForUser($user); + } + } + + /** + * @param Account $account + */ + private function upgradeLiability(Account $account): void + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($account->user); + Log::debug(sprintf('Upgrade liability #%d', $account->id)); + + // get opening balance, and correct if necessary. + $openingBalance = $repository->getOpeningBalance($account); + if (null !== $openingBalance) { + // correct if necessary + $this->correctOpeningBalance($account, $openingBalance); + } + + // add liability direction property (if it does not yet exist!) + $value = $repository->getMetaValue($account, 'liability_direction'); + if (null === $value) { + /** @var AccountMetaFactory $factory */ + $factory = app(AccountMetaFactory::class); + $factory->crud($account, 'liability_direction', 'debit'); + } + } } diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php b/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php index 372c955fbb..46f5c19283 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php +++ b/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php @@ -83,115 +83,6 @@ class UpgradeLiabilitiesEight extends Command return 0; } - /** - * @return bool - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - private function isExecuted(): bool - { - $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - if (null !== $configVar) { - return (bool)$configVar->data; - } - - return false; - } - - /** - * - */ - private function upgradeLiabilities(): void - { - Log::debug('Upgrading liabilities.'); - $users = User::get(); - /** @var User $user */ - foreach ($users as $user) { - $this->upgradeForUser($user); - } - } - - /** - * @param User $user - */ - private function upgradeForUser(User $user): void - { - Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); - $accounts = $user->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', config('firefly.valid_liabilities')) - ->get(['accounts.*']); - /** @var Account $account */ - foreach ($accounts as $account) { - $this->upgradeLiability($account); - $service = app(CreditRecalculateService::class); - $service->setAccount($account); - $service->recalculate(); - } - } - - /** - * @param Account $account - */ - private function upgradeLiability(Account $account): void - { - Log::debug(sprintf('Upgrade liability #%d ("%s")', $account->id, $account->name)); - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($account->user); - - $direction = $repository->getMetaValue($account, 'liability_direction'); - if ('debit' === $direction) { - Log::debug('Direction is debit ("I owe this amount"), so no need to upgrade.'); - } - if ('credit' === $direction && $this->hasBadOpening($account)) { - $this->deleteCreditTransaction($account); - $this->reverseOpeningBalance($account); - $this->line(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name)); - } - if ('credit' === $direction) { - $count = $this->deleteTransactions($account); - if ($count > 0) { - $this->line(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name)); - } - } - Log::debug(sprintf('Done upgrading liability #%d ("%s")', $account->id, $account->name)); - } - - /** - * @param Account $account - * @return bool - */ - private function hasBadOpening(Account $account): bool - { - $openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); - $liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first(); - $openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $account->id) - ->where('transaction_journals.transaction_type_id', $openingBalanceType->id) - ->first(['transaction_journals.*']); - if (null === $openingJournal) { - Log::debug('Account has no opening balance and can be skipped.'); - return false; - } - $liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $account->id) - ->where('transaction_journals.transaction_type_id', $liabilityType->id) - ->first(['transaction_journals.*']); - if (null === $liabilityJournal) { - Log::debug('Account has no liability credit and can be skipped.'); - return false; - } - if (!$openingJournal->date->isSameDay($liabilityJournal->date)) { - Log::debug('Account has opening/credit not on the same day.'); - return false; - } - Log::debug('Account has bad opening balance data.'); - - return true; - } - /** * @param Account $account * @return void @@ -214,35 +105,6 @@ class UpgradeLiabilitiesEight extends Command Log::debug('No liability credit journal found.'); } - /** - * @param Account $account - * @return void - */ - private function reverseOpeningBalance(Account $account): void - { - $openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); - /** @var TransactionJournal $openingJournal */ - $openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $account->id) - ->where('transaction_journals.transaction_type_id', $openingBalanceType->id) - ->first(['transaction_journals.*']); - /** @var Transaction|null $source */ - $source = $openingJournal->transactions()->where('amount', '<', 0)->first(); - /** @var Transaction|null $dest */ - $dest = $openingJournal->transactions()->where('amount', '>', 0)->first(); - if ($source && $dest) { - $sourceId = $source->account_id; - $destId = $dest->account_id; - $dest->account_id = $sourceId; - $source->account_id = $destId; - $source->save(); - $dest->save(); - Log::debug(sprintf('Opening balance transaction journal #%d reversed.', $openingJournal->id)); - return; - } - Log::warning('Did not find opening balance.'); - } - /** * @param $account * @return int @@ -293,6 +155,54 @@ class UpgradeLiabilitiesEight extends Command return $count; } + /** + * @param Account $account + * @return bool + */ + private function hasBadOpening(Account $account): bool + { + $openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); + $liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first(); + $openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $openingBalanceType->id) + ->first(['transaction_journals.*']); + if (null === $openingJournal) { + Log::debug('Account has no opening balance and can be skipped.'); + return false; + } + $liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $liabilityType->id) + ->first(['transaction_journals.*']); + if (null === $liabilityJournal) { + Log::debug('Account has no liability credit and can be skipped.'); + return false; + } + if (!$openingJournal->date->isSameDay($liabilityJournal->date)) { + Log::debug('Account has opening/credit not on the same day.'); + return false; + } + Log::debug('Account has bad opening balance data.'); + + return true; + } + + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + /** * */ @@ -300,4 +210,94 @@ class UpgradeLiabilitiesEight extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } + + /** + * @param Account $account + * @return void + */ + private function reverseOpeningBalance(Account $account): void + { + $openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); + /** @var TransactionJournal $openingJournal */ + $openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $openingBalanceType->id) + ->first(['transaction_journals.*']); + /** @var Transaction|null $source */ + $source = $openingJournal->transactions()->where('amount', '<', 0)->first(); + /** @var Transaction|null $dest */ + $dest = $openingJournal->transactions()->where('amount', '>', 0)->first(); + if ($source && $dest) { + $sourceId = $source->account_id; + $destId = $dest->account_id; + $dest->account_id = $sourceId; + $source->account_id = $destId; + $source->save(); + $dest->save(); + Log::debug(sprintf('Opening balance transaction journal #%d reversed.', $openingJournal->id)); + return; + } + Log::warning('Did not find opening balance.'); + } + + /** + * @param User $user + */ + private function upgradeForUser(User $user): void + { + Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); + $accounts = $user->accounts() + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + $this->upgradeLiability($account); + $service = app(CreditRecalculateService::class); + $service->setAccount($account); + $service->recalculate(); + } + } + + /** + * + */ + private function upgradeLiabilities(): void + { + Log::debug('Upgrading liabilities.'); + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + $this->upgradeForUser($user); + } + } + + /** + * @param Account $account + */ + private function upgradeLiability(Account $account): void + { + Log::debug(sprintf('Upgrade liability #%d ("%s")', $account->id, $account->name)); + + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($account->user); + + $direction = $repository->getMetaValue($account, 'liability_direction'); + if ('debit' === $direction) { + Log::debug('Direction is debit ("I owe this amount"), so no need to upgrade.'); + } + if ('credit' === $direction && $this->hasBadOpening($account)) { + $this->deleteCreditTransaction($account); + $this->reverseOpeningBalance($account); + $this->line(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name)); + } + if ('credit' === $direction) { + $count = $this->deleteTransactions($account); + if ($count > 0) { + $this->line(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name)); + } + } + Log::debug(sprintf('Done upgrading liability #%d ("%s")', $account->id, $account->name)); + } } diff --git a/app/Handlers/Events/Model/BudgetLimitHandler.php b/app/Handlers/Events/Model/BudgetLimitHandler.php index e9d5748589..1a097bca15 100644 --- a/app/Handlers/Events/Model/BudgetLimitHandler.php +++ b/app/Handlers/Events/Model/BudgetLimitHandler.php @@ -146,7 +146,7 @@ class BudgetLimitHandler { if(0 === (int)$budgetLimit->id) { return '0'; - } + } $limitPeriod = Period::make( $budgetLimit->start_date, $budgetLimit->end_date, @@ -217,10 +217,10 @@ class BudgetLimitHandler if ($currentPeriod->equals($limitPeriod)) { $amount = 0 === (int)$budgetLimit->id ? '0' : $budgetLimit->amount; } - if(0 === bccomp($amount,'0')) { + if(0 === bccomp($amount, '0')) { Log::debug('Amount is zero, will not create AB.'); } - if(0 !== bccomp($amount,'0')) { + if(0 !== bccomp($amount, '0')) { Log::debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d'))); $availableBudget = new AvailableBudget( [ diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index 128bb4b4b7..a05537bf99 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -70,6 +70,8 @@ class AutoBudget extends Model public const AUTO_BUDGET_RESET = 1; public const AUTO_BUDGET_ROLLOVER = 2; + protected $fillable = ['budget_id','amount','period']; + /** * @return BelongsTo */ diff --git a/app/Transformers/V2/TransactionGroupTransformer.php b/app/Transformers/V2/TransactionGroupTransformer.php index d0e50f47a7..962e7d5a71 100644 --- a/app/Transformers/V2/TransactionGroupTransformer.php +++ b/app/Transformers/V2/TransactionGroupTransformer.php @@ -273,6 +273,7 @@ class TransactionGroupTransformer extends AbstractTransformer if (10 === strlen($string)) { return Carbon::createFromFormat('Y-m-d', $string, config('app.timezone')); } - return Carbon::createFromFormat('Y-m-d H:i:s', $string, config('app.timezone')); + // 2022-01-01 01:01:01 + return Carbon::createFromFormat('Y-m-d H:i:s', substr($string, 0, 19), config('app.timezone')); } }