diff --git a/app/Console/Commands/Correction/CorrectDatabase.php b/app/Console/Commands/Correction/CorrectDatabase.php new file mode 100644 index 0000000000..189f4a7801 --- /dev/null +++ b/app/Console/Commands/Correction/CorrectDatabase.php @@ -0,0 +1,81 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Console\Commands\Correction; + + +use Artisan; +use Illuminate\Console\Command; +use Schema; + +/** + * Class CorrectDatabase + */ +class CorrectDatabase extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Will correct the integrity of your database, of necessary.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:correct-database'; + + /** + * Execute the console command. + */ + public function handle(): int + { + // if table does not exist, return false + if (!Schema::hasTable('users')) { + return 1; + } + $commands = [ + 'firefly-iii:fix-piggies', + 'firefly-iii:create-link-types', + 'firefly-iii:create-access-tokens', + 'firefly-iii:remove-bills', + 'firefly-iii:enable-currencies', + 'firefly-iii:fix-transfer-budgets', + 'firefly-iii:fix-uneven-amount', + 'firefly-iii:delete-zero-amount', + 'firefly-iii:delete-orphaned-transactions', + 'firefly-iii:delete-empty-journals', + 'firefly-iii:delete-empty-groups', + 'firefly-iii:fix-account-types', + ]; + foreach ($commands as $command) { + $this->line(sprintf('Now executing %s', $command)); + Artisan::call($command); + $result = Artisan::output(); + echo $result; + } + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/Correction/CorrectionSkeleton.php.stub b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub new file mode 100644 index 0000000000..1e514b5177 --- /dev/null +++ b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub @@ -0,0 +1,57 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use Illuminate\Console\Command; + +/** + * Class CorrectionSkeleton + */ +class CorrectionSkeleton extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'DESCRIPTION HERE'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:CORR_COMMAND'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + // + $this->warn('Congrats, you found the skeleton command. Boo!'); + + + return 0; + } +} diff --git a/app/Console/Commands/Correction/CreateAccessTokens.php b/app/Console/Commands/Correction/CreateAccessTokens.php new file mode 100644 index 0000000000..44d92f39fb --- /dev/null +++ b/app/Console/Commands/Correction/CreateAccessTokens.php @@ -0,0 +1,72 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use Exception; +use FireflyIII\User; +use Illuminate\Console\Command; + +/** + * Class CreateAccessTokens + */ +class CreateAccessTokens extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Creates user access tokens.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:create-access-tokens'; + + /** + * Execute the console command. + * + * @return int + * @throws Exception + */ + public function handle(): int + { + $count = 0; + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + $pref = app('preferences')->getForUser($user, 'access_token', null); + if (null === $pref) { + $token = $user->generateAccessToken(); + app('preferences')->setForUser($user, 'access_token', $token); + $this->line(sprintf('Generated access token for user %s', $user->email)); + ++$count; + } + } + if (0 === $count) { + $this->info('All access tokens OK!'); + } + + return 0; + } +} diff --git a/app/Console/Commands/Correction/CreateLinkTypes.php b/app/Console/Commands/Correction/CreateLinkTypes.php new file mode 100644 index 0000000000..83799d7ef7 --- /dev/null +++ b/app/Console/Commands/Correction/CreateLinkTypes.php @@ -0,0 +1,79 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\LinkType; +use Illuminate\Console\Command; + +/** + * Class CreateLinkTypes. Created all link types in case a migration hasn't fired. + */ +class CreateLinkTypes extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Creates all link types.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:create-link-types'; + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle(): int + { + // + $count = 0; + $set = [ + 'Related' => ['relates to', 'relates to'], + 'Refund' => ['(partially) refunds', 'is (partially) refunded by'], + 'Paid' => ['(partially) pays for', 'is (partially) paid for by'], + 'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'], + ]; + foreach ($set as $name => $values) { + $link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first(); + if (null === $link) { + $link = new LinkType; + $link->name = $name; + $link->outward = $values[0]; + $link->inward = $values[1]; + ++$count; + $this->line(sprintf('Created missing link type "%s"', $name)); + } + $link->editable = false; + $link->save(); + } + if (0 === $count) { + $this->info('All link types OK!'); + } + + return 0; + } +} diff --git a/app/Console/Commands/Correction/DeleteEmptyGroups.php b/app/Console/Commands/Correction/DeleteEmptyGroups.php new file mode 100644 index 0000000000..c828bc2719 --- /dev/null +++ b/app/Console/Commands/Correction/DeleteEmptyGroups.php @@ -0,0 +1,68 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use Exception; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; + +/** + * Class DeleteEmptyGroups + */ +class DeleteEmptyGroups extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Delete empty transaction groups.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:delete-empty-groups'; + + /** + * Execute the console command. + * + * @throws Exception; + * @return mixed + */ + public function handle(): int + { + // + $groups = array_unique(TransactionJournal::get(['transaction_group_id'])->pluck('transaction_group_id')->toArray()); + $count = TransactionGroup::whereNull('deleted_at')->whereNotIn('id', $groups)->count(); + if (0 === $count) { + $this->info('No empty groups.'); + } + if ($count > 0) { + $this->info(sprintf('Deleted %d empty groups.', $count)); + TransactionGroup::whereNull('deleted_at')->whereNotIn('id', $groups)->delete(); + } + + return 0; + } +} diff --git a/app/Console/Commands/Correction/DeleteEmptyJournals.php b/app/Console/Commands/Correction/DeleteEmptyJournals.php new file mode 100644 index 0000000000..ac60ae6775 --- /dev/null +++ b/app/Console/Commands/Correction/DeleteEmptyJournals.php @@ -0,0 +1,115 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use DB; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; + +/** + * Class DeleteEmptyJournals + */ +class DeleteEmptyJournals extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Delete empty and uneven transaction journals.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:delete-empty-journals'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $this->deleteUnevenJournals(); + $this->deleteEmptyJournals(); + + + return 0; + } + + private function deleteEmptyJournals(): void + { + + $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) { + TransactionJournal::find($entry->id)->delete(); + $this->info(sprintf('Deleted empty transaction #%d', $entry->id)); + ++$count; + } + if (0 === $count) { + $this->info('No empty transactions.'); + } + } + + /** + * Delete transactions and their journals if they have an uneven number of transactions. + */ + private function deleteUnevenJournals(): void + { + /** + * select count(transactions.transaction_journal_id) as the_count, transactions.transaction_journal_id from transactions + * + * where transactions.deleted_at is null + * + * group by transactions.transaction_journal_id + * having the_count in () + */ + + $set = Transaction + ::whereNull('deleted_at') + ->having('the_count', '!=', '2') + ->groupBy('transactions.transaction_journal_id') + ->get([DB::raw('COUNT(transactions.transaction_journal_id) as the_count'), 'transaction_journal_id']); + $total = 0; + foreach ($set as $row) { + $count = (int)$row->the_count; + if (1 === $count % 2) { + // uneven number, delete journal and transactions: + TransactionJournal::find((int)$row->transaction_journal_id)->delete(); + Transaction::where('transaction_journal_id', (int)$row->transaction_journal_id)->delete(); + $this->info(sprintf('Deleted transaction #%d because it had an uneven number of transactions.', $row->transaction_journal_id)); + $total++; + } + } + if (0 === $total) { + $this->info('No uneven transactions.'); + } + } + +} diff --git a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php new file mode 100644 index 0000000000..52d02379b9 --- /dev/null +++ b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php @@ -0,0 +1,126 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use Exception; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; +use stdClass; + +/** + * Deletes transactions where the journal has been deleted. + */ +class DeleteOrphanedTransactions extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Deletes orphaned transactions.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:delete-orphaned-transactions'; + + /** + * Execute the console command. + * + * @return int + * @throws Exception + */ + public function handle(): int + { + $this->deleteOrphanedTransactions(); + $this->deleteFromOrphanedAccounts(); + + + 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 #%d because account #%d was already deleted.', $transaction->transaction_journal_id, $transaction->account_id) + ); + $count++; + } + if(0===$count) { + $this->info('No orphaned accounts.'); + } + } + + /** + * @throws Exception + */ + private function deleteOrphanedTransactions(): void + { + $count = 0; + $set = Transaction + ::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNotNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNotNull('transactions.id') + ->get( + [ + 'transaction_journals.id as journal_id', + 'transactions.id as transaction_id', + ] + ); + /** @var stdClass $entry */ + foreach ($set as $entry) { + $transaction = Transaction::find((int)$entry->transaction_id); + $transaction->delete(); + $this->info( + sprintf( + 'Transaction #%d (part of deleted journal #%d) has been deleted as well.', + $entry->transaction_id, + $entry->journal_id + ) + ); + ++$count; + } + if (0 === $count) { + $this->info('No orphaned transactions.'); + } + } +} diff --git a/app/Console/Commands/Correction/DeleteZeroAmount.php b/app/Console/Commands/Correction/DeleteZeroAmount.php new file mode 100644 index 0000000000..a3e33554dc --- /dev/null +++ b/app/Console/Commands/Correction/DeleteZeroAmount.php @@ -0,0 +1,71 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Transaction; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Exception; + +/** + * Class DeleteZeroAmount + */ +class DeleteZeroAmount extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Delete transactions with zero amount.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:delete-zero-amount'; + + /** + * Execute the console command. + * @throws Exception + * @return int + */ + public function handle(): int + { + $set = Transaction::where('amount', 0)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); + $set = array_unique($set); + /** @var Collection $journals */ + $journals = TransactionJournal::whereIn('id', $set)->get(); + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + $this->info(sprintf('Deleted transaction #%d because the amount is zero (0.00).', $journal->id)); + $journal->delete(); + Transaction::where('transaction_journal_id', $journal->id)->delete(); + } + if (0 === $journals->count()) { + $this->info('No zero-amount transactions.'); + } + + return 0; + } +} diff --git a/app/Console/Commands/Correction/EnableCurrencies.php b/app/Console/Commands/Correction/EnableCurrencies.php new file mode 100644 index 0000000000..f338b949b2 --- /dev/null +++ b/app/Console/Commands/Correction/EnableCurrencies.php @@ -0,0 +1,96 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\AccountMeta; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; + +/** + * Class EnableCurrencies + */ +class EnableCurrencies extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Enables all currencies in use.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:enable-currencies'; + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle(): int + { + $found = []; + // get all meta entries + /** @var Collection $meta */ + $meta = AccountMeta::where('name', 'currency_id')->groupBy('data')->get(['data']); + foreach ($meta as $entry) { + $found[] = (int)$entry->data; + } + + // get all from journals: + /** @var Collection $journals */ + $journals = TransactionJournal::groupBy('transaction_currency_id')->get(['transaction_currency_id']); + foreach ($journals as $entry) { + $found[] = (int)$entry->transaction_currency_id; + } + + // get all from transactions + /** @var Collection $transactions */ + $transactions = Transaction::groupBy('transaction_currency_id')->get(['transaction_currency_id']); + foreach ($transactions as $entry) { + $found[] = (int)$entry->transaction_currency_id; + } + + // get all from budget limits + /** @var Collection $limits */ + $limits = BudgetLimit::groupBy('transaction_currency_id')->get(['transaction_currency_id']); + foreach ($limits as $entry) { + $found[] = (int)$entry->transaction_currency_id; + } + + $found = array_unique($found); + $this->info(sprintf('%d different currencies are currently in use.', count($found))); + $disabled = TransactionCurrency::whereIn('id', $found)->where('enabled', false)->count(); + if ($disabled > 0) { + $this->info(sprintf('%d were still disabled. This has been corrected.', $disabled)); + } + TransactionCurrency::whereIn('id', $found)->update(['enabled' => true]); + + return 0; + } +} diff --git a/app/Console/Commands/Correction/FixAccountTypes.php b/app/Console/Commands/Correction/FixAccountTypes.php new file mode 100644 index 0000000000..958bbf3c1d --- /dev/null +++ b/app/Console/Commands/Correction/FixAccountTypes.php @@ -0,0 +1,113 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; + +/** + * Class FixAccountTypes + */ +class FixAccountTypes extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Make sure all journals have the correct from/to account types.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:fix-account-types'; + /** @var array */ + private $expected; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $this->expected = config('firefly.source_dests'); + $journals = TransactionJournal::get(); + foreach ($journals as $journal) { + $this->inspectJournal($journal); + } + + return 0; + } + + private function getDestinationAccount(TransactionJournal $journal): Account + { + return $journal->transactions()->where('amount', '>', 0)->first()->account; + } + + /** + * @param TransactionJournal $journal + * + * @return Account + */ + private function getSourceAccount(TransactionJournal $journal): Account + { + return $journal->transactions()->where('amount', '<', 0)->first()->account; + } + + /** + * @param TransactionJournal $journal + */ + private function inspectJournal(TransactionJournal $journal): void + { + $count = $journal->transactions()->count(); + if (2 !== $count) { + $this->info(sprintf('Cannot inspect journal #%d because it does not have 2 transactions, but %d', $journal->id, $count)); + + return; + } + $type = $journal->transactionType->type; + $sourceAccount = $this->getSourceAccount($journal); + $sourceAccountType = $sourceAccount->accountType->type; + $destinationAccount = $this->getDestinationAccount($journal); + $destinationAccountType = $destinationAccount->accountType->type; + if (!isset($this->expected[$type])) { + $this->info(sprintf('No source/destination info for transaction type %s.', $type)); + + return; + } + if (!isset($this->expected[$type][$sourceAccountType])) { + $this->info(sprintf('The source of %s #%d cannot be of type "%s".', $type, $journal->id, $sourceAccountType)); + $this->info(sprintf('The destination of %s #%d probably cannot be of type "%s".', $type, $journal->id, $destinationAccountType)); + + // TODO think of a way to fix the problem. + return; + } + $expectedTypes = $this->expected[$type][$sourceAccountType]; + if (!\in_array($destinationAccountType, $expectedTypes, true)) { + $this->info(sprintf('The destination of %s #%d cannot be of type "%s".', $type, $journal->id, $destinationAccountType)); + // TODO think of a way to fix the problem. + } + } +} diff --git a/app/Console/Commands/Correction/FixPiggies.php b/app/Console/Commands/Correction/FixPiggies.php new file mode 100644 index 0000000000..f07d6f77af --- /dev/null +++ b/app/Console/Commands/Correction/FixPiggies.php @@ -0,0 +1,82 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\PiggyBankEvent; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Illuminate\Console\Command; + +/** + * Report (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events. + * + * Class FixPiggies + */ +class FixPiggies extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Fixes common issues with piggy banks.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:fix-piggies'; + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle(): int + { + $set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get(); + $set->each( + function (PiggyBankEvent $event) { + if (null === $event->transaction_journal_id) { + return true; + } + /** @var TransactionJournal $journal */ + $journal = $event->transactionJournal()->first(); + if (null === $journal) { + return true; + } + + $type = $journal->transactionType->type; + if (TransactionType::TRANSFER !== $type) { + $event->transaction_journal_id = null; + $event->save(); + $this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id)); + } + + return true; + } + ); + $this->line(sprintf('Verified the content of %d piggy bank events.', $set->count())); + + return 0; + } +} diff --git a/app/Console/Commands/Correction/FixUnevenAmount.php b/app/Console/Commands/Correction/FixUnevenAmount.php new file mode 100644 index 0000000000..16390769f6 --- /dev/null +++ b/app/Console/Commands/Correction/FixUnevenAmount.php @@ -0,0 +1,99 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use DB; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; +use stdClass; + +/** + * Class FixUnevenAmount + */ +class FixUnevenAmount extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Fix journals with uneven amounts.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:fix-uneven-amount'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + + $count = 0; + // get invalid journals + $journals = DB::table('transactions') + ->groupBy('transaction_journal_id') + ->whereNull('deleted_at') + ->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')]); + /** @var stdClass $entry */ + foreach ($journals as $entry) { + if (0 !== bccomp((string)$entry->the_sum, '0')) { + $this->fixJournal((int)$entry->transaction_journal_id); + $count++; + } + } + if (0 === $count) { + $this->info('Amount integrity OK!'); + } + + return 0; + } + + /** + * @param int $param + */ + private function fixJournal(int $param): void + { + // one of the transactions is bad. + $journal = TransactionJournal::find($param); + if (!$journal) { + return; + } + /** @var Transaction $source */ + $source = $journal->transactions()->where('amount', '<', 0)->first(); + $amount = bcmul('-1', (string)$source->amount); + + // fix amount of destination: + /** @var Transaction $destination */ + $destination = $journal->transactions()->where('amount', '>', 0)->first(); + $destination->amount = $amount; + $destination->save(); + + $this->line(sprintf('Corrected amount in transaction #%d', $param)); + + } +} diff --git a/app/Console/Commands/Correction/RemoveBills.php b/app/Console/Commands/Correction/RemoveBills.php new file mode 100644 index 0000000000..9df66bcbc9 --- /dev/null +++ b/app/Console/Commands/Correction/RemoveBills.php @@ -0,0 +1,71 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Illuminate\Console\Command; + +/** + * Class RemoveBills + */ +class RemoveBills extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Remove bills from transactions that shouldn\'t have one.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:remove-bills'; + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle(): int + { + /** @var TransactionType $withdrawal */ + $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + $journals = TransactionJournal::whereNotNull('bill_id')->where('transaction_type_id', '!=', $withdrawal->id)->get(); + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + $this->line(sprintf('Transaction journal #%d should not be linked to bill #%d.', $journal->id, $journal->bill_id)); + $journal->bill_id = null; + $journal->save(); + } + if (0 === $journals->count()) { + $this->info('All transactions have correct bill information.'); + } + if ($journals->count() > 0) { + $this->info('Fixed all transactions so they have correct bill information.'); + } + + return 0; + } +} diff --git a/app/Console/Commands/Correction/TransferBudgets.php b/app/Console/Commands/Correction/TransferBudgets.php new file mode 100644 index 0000000000..4eea03ddf0 --- /dev/null +++ b/app/Console/Commands/Correction/TransferBudgets.php @@ -0,0 +1,71 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Illuminate\Console\Command; + +/** + * Class TransferBudgets + */ +class TransferBudgets extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Removes budgets from transfers.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:fix-transfer-budgets'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $set = TransactionJournal::distinct() + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id') + ->whereNotIn('transaction_types.type', [TransactionType::WITHDRAWAL]) + ->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']); + $count = 0; + /** @var TransactionJournal $entry */ + foreach ($set as $entry) { + $this->info(sprintf('Transaction #%d is a %s, so has no longer a budget.', $entry->id, $entry->transactionType->type)); + $entry->budgets()->sync([]); + $count++; + } + if (0 === $count) { + $this->info('No invalid budget/journal entries.'); + } + + return 0; + } +} diff --git a/app/Console/Commands/Integrity/ReportEmptyObjects.php b/app/Console/Commands/Integrity/ReportEmptyObjects.php new file mode 100644 index 0000000000..1c24e3783f --- /dev/null +++ b/app/Console/Commands/Integrity/ReportEmptyObjects.php @@ -0,0 +1,191 @@ +. + */ + +namespace FireflyIII\Console\Commands\Integrity; + +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Account; +use Illuminate\Console\Command; +use stdClass; + +/** + * Class ReportEmptyObjects + */ +class ReportEmptyObjects extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Reports on empty database objects.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:report-empty-objects'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $this->reportEmptyBudgets(); + $this->reportEmptyCategories(); + $this->reportEmptyTags(); + $this->reportAccounts(); + $this->reportBudgetLimits(); + return 0; + } + + + + /** + * 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); + } + } + + /** + * 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) { + $name = $entry->name; + $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.'; + $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name); + $this->line($line); + } + } + + /** + * Report on budgets with no transactions or journals. + */ + private function reportEmptyBudgets(): void + { + $set = Budget::leftJoin('budget_transaction_journal', 'budgets.id', '=', 'budget_transaction_journal.budget_id') + ->leftJoin('users', 'budgets.user_id', '=', 'users.id') + ->distinct() + ->whereNull('budget_transaction_journal.budget_id') + ->whereNull('budgets.deleted_at') + ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']); + + /** @var stdClass $entry */ + foreach ($set as $entry) { + $objName = $entry->name; + $line = sprintf( + 'User #%d (%s) has budget #%d ("%s") which has no transactions.', + $entry->user_id, + $entry->email, + $entry->id, + $objName + ); + $this->line($line); + } + } + + /** + * Report on categories with no transactions or journals. + */ + private function reportEmptyCategories(): void + { + $set = Category::leftJoin('category_transaction_journal', 'categories.id', '=', 'category_transaction_journal.category_id') + ->leftJoin('users', 'categories.user_id', '=', 'users.id') + ->distinct() + ->whereNull('category_transaction_journal.category_id') + ->whereNull('categories.deleted_at') + ->get(['categories.id', 'categories.name', 'categories.user_id', 'users.email']); + + /** @var stdClass $entry */ + foreach ($set as $entry) { + $objName = $entry->name; + + $line = sprintf( + 'User #%d (%s) has category #%d ("%s") which has no transactions.', + $entry->user_id, + $entry->email, + $entry->id, + $objName + ); + $this->line($line); + } + } + + /** + * + */ + private function reportEmptyTags(): void + { + $set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id') + ->leftJoin('users', 'tags.user_id', '=', 'users.id') + ->distinct() + ->whereNull('tag_transaction_journal.tag_id') + ->whereNull('tags.deleted_at') + ->get(['tags.id', 'tags.tag', 'tags.user_id', 'users.email']); + + /** @var stdClass $entry */ + foreach ($set as $entry) { + $objName = $entry->tag; + + $line = sprintf( + 'User #%d (%s) has tag #%d ("%s") which has no transactions.', + $entry->user_id, + $entry->email, + $entry->id, + $objName + ); + $this->line($line); + } + } +} diff --git a/app/Console/Commands/Integrity/ReportIntegrity.php b/app/Console/Commands/Integrity/ReportIntegrity.php new file mode 100644 index 0000000000..7ffac64eb3 --- /dev/null +++ b/app/Console/Commands/Integrity/ReportIntegrity.php @@ -0,0 +1,108 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Console\Commands\Integrity; + + +use Illuminate\Console\Command; +use Schema; +use Artisan; + +/** + * Class ReportIntegrity + */ +class ReportIntegrity extends Command +{ + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Will report on the integrity of your database.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:report-integrity'; + + /** + * Execute the console command. + */ + public function handle(): int + { + // if table does not exist, return false + if (!Schema::hasTable('users')) { + return 1; + } + $commands = [ + 'firefly-iii:report-empty-objects', + 'firefly-iii:report-sum', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', +// 'firefly-iii:', + ]; + foreach ($commands as $command) { + $this->line(sprintf('Now executing %s', $command)); + Artisan::call($command); + $result = Artisan::output(); + echo $result; + } + +// $this->reportEmptyBudgets(); +// $this->reportEmptyCategories(); +// $this->reportObject('tag'); +// $this->reportAccounts(); +// $this->reportBudgetLimits(); +// $this->reportSum(); +// $this->reportJournals(); +// $this->reportTransactions(); +// $this->reportDeletedAccounts(); +// $this->reportNoTransactions(); +// $this->reportTransfersBudgets(); +// $this->reportIncorrectJournals(); +// $this->repairPiggyBanks(); +// $this->createLinkTypes(); +// $this->createAccessTokens(); +// $this->fixDoubleAmounts(); // is a report function! +// $this->fixBadMeta(); +// $this->removeBills(); +// $this->enableCurrencies(); +// $this->reportZeroAmount(); + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/Integrity/ReportSkeleton.php.stub b/app/Console/Commands/Integrity/ReportSkeleton.php.stub new file mode 100644 index 0000000000..16b26c0ab0 --- /dev/null +++ b/app/Console/Commands/Integrity/ReportSkeleton.php.stub @@ -0,0 +1,56 @@ +. + */ + +namespace FireflyIII\Console\Commands\Integrity; + +use Illuminate\Console\Command; + +/** + * Class ReportSkeleton + */ +class ReportSkeleton extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'DESCRIPTION HERE'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:INT_COMMAND'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + // + $this->warn('Congrats, you found the skeleton command. Boo!'); + + return 0; + } +} diff --git a/app/Console/Commands/Integrity/ReportSum.php b/app/Console/Commands/Integrity/ReportSum.php new file mode 100644 index 0000000000..bba9c46550 --- /dev/null +++ b/app/Console/Commands/Integrity/ReportSum.php @@ -0,0 +1,78 @@ +. + */ + +namespace FireflyIII\Console\Commands\Integrity; + +use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\User; +use Illuminate\Console\Command; + +/** + * Class ReportSkeleton + */ +class ReportSum extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Report on the total sum of transactions. Must be 0.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:report-sum'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $this->reportSum(); + + return 0; + } + + + /** + * Reports for each user when the sum of their transactions is not zero. + */ + private function reportSum(): void + { + /** @var UserRepositoryInterface $userRepository */ + $userRepository = app(UserRepositoryInterface::class); + + /** @var User $user */ + foreach ($userRepository->all() as $user) { + $sum = (string)$user->transactions()->sum('amount'); + if (0 !== bccomp($sum, '0')) { + $this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!'); + } + if (0 === bccomp($sum, '0')) { + $this->info(sprintf('Amount integrity OK for user #%d', $user->id)); + } + } + } +} diff --git a/app/Console/Commands/Upgrade/JournalCurrencies.php b/app/Console/Commands/Upgrade/JournalCurrencies.php index 48903d0301..aea1685cc3 100644 --- a/app/Console/Commands/Upgrade/JournalCurrencies.php +++ b/app/Console/Commands/Upgrade/JournalCurrencies.php @@ -206,6 +206,7 @@ class JournalCurrencies extends Command $accountRepos->setUser($transaction->account->user); $currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id')); $journal = $transaction->transactionJournal; + $currencyCode = $journal->transactionCurrency->code ?? '(nothing)'; if (null === $currency) { return; @@ -218,7 +219,7 @@ class JournalCurrencies extends Command $journal->id, $journal->description, $currency->code, - $journal->transactionCurrency->code + $currencyCode ) ); $journal->transaction_currency_id = $currency->id; diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php index 856057ebaa..b144d069ad 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -24,6 +24,7 @@ namespace FireflyIII\Console\Commands\Upgrade; use Exception; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Services\Internal\Destroy\JournalDestroyService; @@ -93,7 +94,8 @@ class MigrateToGroups extends Command } Log::debug('---- start group migration ----'); - $this->makeGroups(); + $this->makeGroupsFromSplitJournals(); + $this->makeGroupsFromAll(); Log::debug('---- end group migration ----'); $this->markAsMigrated(); @@ -101,6 +103,19 @@ class MigrateToGroups extends Command return 0; } + /** + * @param TransactionJournal $journal + */ + private function giveGroup(TransactionJournal $journal): void + { + $group = new TransactionGroup; + $group->title = null; + $group->user_id = $journal->user_id; + $group->save(); + $journal->transaction_group_id = $group->id; + $journal->save(); + } + /** * @return bool */ @@ -114,12 +129,50 @@ class MigrateToGroups extends Command return false; // @codeCoverageIgnore } + /** + * Gives all journals without a group a group. + */ + private function makeGroupsFromAll(): void + { + $orphanedJournals = $this->journalRepository->getJournalsWithoutGroup(); + if ($orphanedJournals->count() > 0) { + Log::debug(sprintf('Going to convert %d transactions. Please hold..', $orphanedJournals->count())); + /** @var TransactionJournal $journal */ + foreach ($orphanedJournals as $journal) { + $this->giveGroup($journal); + } + } + if (0 === $orphanedJournals->count()) { + $this->info('No need to convert transactions.'); + } + } + + /** + * + * @throws Exception + */ + private function makeGroupsFromSplitJournals(): void + { + $splitJournals = $this->journalRepository->getSplitJournals(); + + if ($splitJournals->count() > 0) { + $this->info(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count())); + /** @var TransactionJournal $journal */ + foreach ($splitJournals as $journal) { + $this->makeMultiGroup($journal); + } + } + if (0 === $splitJournals->count()) { + $this->info('Found no split transactions. Nothing to do.'); + } + } + /** * @param TransactionJournal $journal * * @throws Exception */ - private function makeGroup(TransactionJournal $journal): void + private function makeMultiGroup(TransactionJournal $journal): void { // double check transaction count. if ($journal->transactions->count() <= 2) { @@ -204,33 +257,13 @@ class MigrateToGroups extends Command Log::debug('Done calling transaction journal factory'); // delete the old transaction journal. - //$this->service->destroy($journal); + $this->service->destroy($journal); // report on result: Log::debug(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); $this->line(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); } - /** - * - * @throws Exception - */ - private function makeGroups(): void - { - $splitJournals = $this->journalRepository->getSplitJournals(); - - if ($splitJournals->count() > 0) { - $this->info(sprintf('Going to un-split %d transaction(s). This could take some time.', $splitJournals->count())); - /** @var TransactionJournal $journal */ - foreach ($splitJournals as $journal) { - $this->makeGroup($journal); - } - } - if (0 === $splitJournals->count()) { - $this->info('Found no split journals. Nothing to do.'); - } - } - /** * */ diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradeDatabase.php index d5a8c082e6..4b1675c279 100644 --- a/app/Console/Commands/Upgrade/UpgradeDatabase.php +++ b/app/Console/Commands/Upgrade/UpgradeDatabase.php @@ -34,7 +34,7 @@ class UpgradeDatabase extends Command * * @var string */ - protected $description = 'Executes all upgrade commands.'; + protected $description = 'Upgrades the database to the latest version.'; /** * The name and signature of the console command. * diff --git a/app/Console/Commands/Upgrade/UpgradeSkeleton.php b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub similarity index 95% rename from app/Console/Commands/Upgrade/UpgradeSkeleton.php rename to app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub index 3f2347d24f..7ff9d96597 100644 --- a/app/Console/Commands/Upgrade/UpgradeSkeleton.php +++ b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub @@ -40,7 +40,7 @@ class UpgradeSkeleton extends Command * * @var string */ - protected $signature = 'firefly-iii:SKELETON {--F|force : Force the execution of this command.}'; + protected $signature = 'firefly-iii:UPGRSKELETON {--F|force : Force the execution of this command.}'; /** * Execute the console command. diff --git a/app/Console/Commands/VerifyDatabase.php b/app/Console/Commands/VerifyDatabase.php deleted file mode 100644 index 07458fe113..0000000000 --- a/app/Console/Commands/VerifyDatabase.php +++ /dev/null @@ -1,714 +0,0 @@ -. - */ - -/** @noinspection PhpDynamicAsStaticMethodCallInspection */ - -declare(strict_types=1); - -namespace FireflyIII\Console\Commands; - -use DB; -use Exception; -use FireflyIII\Models\Account; -use FireflyIII\Models\AccountMeta; -use FireflyIII\Models\AccountType; -use FireflyIII\Models\Budget; -use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\Category; -use FireflyIII\Models\LinkType; -use FireflyIII\Models\PiggyBankEvent; -use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\User\UserRepositoryInterface; -use FireflyIII\User; -use Illuminate\Console\Command; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Collection; -use Log; -use Schema; -use stdClass; - -/** - * Class VerifyDatabase. - * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @codeCoverageIgnore - */ -class VerifyDatabase extends Command -{ - /** - * The console command description. - * - * @var string - */ - protected $description = 'Will verify your database.'; - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'firefly:verify'; - - /** - * Execute the console command. - */ - public function handle(): int - { - // if table does not exist, return false - if (!Schema::hasTable('users')) { - return 1; - } - - $this->reportEmptyBudgets(); - $this->reportEmptyCategories(); - $this->reportObject('tag'); - $this->reportAccounts(); - $this->reportBudgetLimits(); - $this->reportSum(); - $this->reportJournals(); - $this->reportTransactions(); - $this->reportDeletedAccounts(); - $this->reportNoTransactions(); - $this->reportTransfersBudgets(); - $this->reportIncorrectJournals(); - $this->repairPiggyBanks(); - $this->createLinkTypes(); - $this->createAccessTokens(); - $this->fixDoubleAmounts(); - $this->fixBadMeta(); - $this->removeBills(); - $this->enableCurrencies(); - $this->reportZeroAmount(); - - return 0; - } - - /** - * Create user access tokens, if not present already. - */ - private function createAccessTokens(): void - { - $count = 0; - $users = User::get(); - /** @var User $user */ - foreach ($users as $user) { - $pref = app('preferences')->getForUser($user, 'access_token', null); - if (null === $pref) { - $token = $user->generateAccessToken(); - app('preferences')->setForUser($user, 'access_token', $token); - $this->line(sprintf('Generated access token for user %s', $user->email)); - ++$count; - } - } - if (0 === $count) { - $this->info('All access tokens OK!'); - } - } - - /** - * Create default link types if necessary. - */ - private function createLinkTypes(): void - { - $count = 0; - $set = [ - 'Related' => ['relates to', 'relates to'], - 'Refund' => ['(partially) refunds', 'is (partially) refunded by'], - 'Paid' => ['(partially) pays for', 'is (partially) paid for by'], - 'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'], - ]; - foreach ($set as $name => $values) { - $link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first(); - if (null === $link) { - $link = new LinkType; - $link->name = $name; - $link->outward = $values[0]; - $link->inward = $values[1]; - ++$count; - } - $link->editable = false; - $link->save(); - } - if (0 === $count) { - $this->info('All link types OK!'); - } - } - - /** - * Will make sure that all currencies in use are actually enabled. - */ - private function enableCurrencies(): void - { - $found = []; - // get all meta entries - /** @var Collection $meta */ - $meta = AccountMeta::where('name', 'currency_id')->groupBy('data')->get(['data']); - foreach ($meta as $entry) { - $found[] = (int)$entry->data; - } - - // get all from journals: - /** @var Collection $journals */ - $journals = TransactionJournal::groupBy('transaction_currency_id')->get(['transaction_currency_id']); - foreach ($journals as $entry) { - $found[] = (int)$entry->transaction_currency_id; - } - - // get all from transactions - /** @var Collection $transactions */ - $transactions = Transaction::groupBy('transaction_currency_id')->get(['transaction_currency_id']); - foreach ($transactions as $entry) { - $found[] = (int)$entry->transaction_currency_id; - } - - // get all from budget limits - /** @var Collection $limits */ - $limits = BudgetLimit::groupBy('transaction_currency_id')->get(['transaction_currency_id']); - foreach ($limits as $entry) { - $found[] = (int)$entry->transaction_currency_id; - } - - $found = array_unique($found); - TransactionCurrency::whereIn('id', $found)->update(['enabled' => true]); - - } - - /** - * Fix the situation where the matching transactions of a journal somehow have non-matching categories or budgets. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - private function fixBadMeta(): void - { - // categories - $set = Transaction - ::leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id') - ->whereNull('transactions.deleted_at') - ->get(['transactions.id', 'transaction_journal_id', 'identifier', 'category_transaction.category_id', 'category_transaction.id as ct_id']); - $results = []; - foreach ($set as $obj) { - $key = $obj->transaction_journal_id . '-' . $obj->identifier; - $category = (int)$obj->category_id; - - // value exists and is not category: - if (isset($results[$key]) && $results[$key] !== $category) { - $this->error( - sprintf( - 'Transaction #%d referred to the wrong category. Was category #%d but is fixed to be category #%d.', $obj->transaction_journal_id, - $category, $results[$key] - ) - ); - DB::table('category_transaction')->where('id', $obj->ct_id)->update(['category_id' => $results[$key]]); - - } - - // value does not exist: - if ($category > 0 && !isset($results[$key])) { - $results[$key] = $category; - } - } - - // budgets - $set = Transaction - ::leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id') - ->whereNull('transactions.deleted_at') - ->get(['transactions.id', 'transaction_journal_id', 'identifier', 'budget_transaction.budget_id', 'budget_transaction.id as ct_id']); - $results = []; - foreach ($set as $obj) { - $key = $obj->transaction_journal_id . '-' . $obj->identifier; - $budget = (int)$obj->budget_id; - - // value exists and is not budget: - if (isset($results[$key]) && $results[$key] !== $budget) { - $this->error( - sprintf( - 'Transaction #%d referred to the wrong budget. Was budget #%d but is fixed to be budget #%d.', $obj->transaction_journal_id, $budget, - $results[$key] - ) - ); - DB::table('budget_transaction')->where('id', $obj->ct_id)->update(['budget_id' => $results[$key]]); - - } - - // value does not exist: - if ($budget > 0 && !isset($results[$key])) { - $results[$key] = $budget; - } - } - } - - /** - * Makes sure amounts are stored correctly. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - private function fixDoubleAmounts(): void - { - $count = 0; - // get invalid journals - $errored = []; - $journals = DB::table('transactions') - ->groupBy('transaction_journal_id') - ->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')]); - /** @var stdClass $entry */ - foreach ($journals as $entry) { - if (0 !== bccomp((string)$entry->the_sum, '0')) { - $errored[] = $entry->transaction_journal_id; - } - } - foreach ($errored as $journalId) { - // select and update: - $res = Transaction::whereNull('deleted_at')->where('transaction_journal_id', $journalId)->groupBy('amount')->get([DB::raw('MIN(id) as first_id')]); - $ids = $res->pluck('first_id')->toArray(); - DB::table('transactions')->whereIn('id', $ids)->update(['amount' => DB::raw('amount * -1')]); - ++$count; - // report about it - /** @var TransactionJournal $journal */ - $journal = TransactionJournal::find($journalId); - if (null === $journal) { - continue; - } - if (TransactionType::OPENING_BALANCE === $journal->transactionType->type) { - $this->error( - sprintf( - 'Transaction #%d was stored incorrectly. One of your asset accounts may show the wrong balance. Please visit /transactions/show/%d to verify the opening balance.', - $journalId, $journalId - ) - ); - } - if (TransactionType::OPENING_BALANCE !== $journal->transactionType->type) { - $this->error( - sprintf( - 'Transaction #%d was stored incorrectly. Could be that the transaction shows the wrong amount. Please visit /transactions/show/%d to verify the opening balance.', - $journalId, $journalId - ) - ); - } - } - if (0 === $count) { - $this->info('Amount integrity OK!'); - } - } - - /** - * Removes bills from journals that should not have bills. - */ - private function removeBills(): void - { - /** @var TransactionType $withdrawal */ - $withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); - $journals = TransactionJournal::whereNotNull('bill_id') - ->where('transaction_type_id', '!=', $withdrawal->id)->get(); - /** @var TransactionJournal $journal */ - foreach ($journals as $journal) { - $this->line(sprintf('Transaction journal #%d should not be linked to bill #%d.', $journal->id, $journal->bill_id)); - $journal->bill_id = null; - $journal->save(); - } - } - - /** - * Eeport (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events. - */ - private function repairPiggyBanks(): void - { - $set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get(); - $set->each( - function (PiggyBankEvent $event) { - if (null === $event->transaction_journal_id) { - return true; - } - /** @var TransactionJournal $journal */ - $journal = $event->transactionJournal()->first(); - if (null === $journal) { - return true; - } - - $type = $journal->transactionType->type; - if (TransactionType::TRANSFER !== $type) { - $event->transaction_journal_id = null; - $event->save(); - $this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id)); - } - - return true; - } - ); - } - - /** - * 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) { - $name = $entry->name; - $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.'; - $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name); - $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); - } - } - - /** - * Reports on deleted accounts that still have not deleted transactions or journals attached to them. - */ - private function reportDeletedAccounts(): void - { - $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNotNull('accounts.deleted_at') - ->whereNotNull('transactions.id') - ->where( - function (Builder $q) { - $q->whereNull('transactions.deleted_at'); - $q->orWhereNull('transaction_journals.deleted_at'); - } - ) - ->get( - ['accounts.id as account_id', 'accounts.deleted_at as account_deleted_at', 'transactions.id as transaction_id', - 'transactions.deleted_at as transaction_deleted_at', 'transaction_journals.id as journal_id', - 'transaction_journals.deleted_at as journal_deleted_at',] - ); - /** @var stdClass $entry */ - foreach ($set as $entry) { - $date = $entry->transaction_deleted_at ?? $entry->journal_deleted_at; - $this->error( - 'Error: Account #' . $entry->account_id . ' should have been deleted, but has not.' . - ' Find it in the table called "accounts" and change the "deleted_at" field to: "' . $date . '"' - ); - } - } - - /** - * Report on budgets with no transactions or journals. - */ - private function reportEmptyBudgets(): void - { - $set = Budget::leftJoin('budget_transaction_journal', 'budgets.id', '=', 'budget_transaction_journal.budget_id') - ->leftJoin('users', 'budgets.user_id', '=', 'users.id') - ->distinct() - ->whereNull('budget_transaction_journal.budget_id') - ->whereNull('budgets.deleted_at') - ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $objName = $entry->name; - - // also count the transactions: - $countTransactions = DB::table('budget_transaction')->where('budget_id', $entry->id)->count(); - - if (0 === $countTransactions) { - $line = sprintf( - 'User #%d (%s) has budget #%d ("%s") which has no transactions.', - $entry->user_id, - $entry->email, - $entry->id, - $objName - ); - $this->line($line); - } - } - } - - /** - * Report on categories with no transactions or journals. - */ - private function reportEmptyCategories(): void - { - $set = Category::leftJoin('category_transaction_journal', 'categories.id', '=', 'category_transaction_journal.category_id') - ->leftJoin('users', 'categories.user_id', '=', 'users.id') - ->distinct() - ->whereNull('category_transaction_journal.category_id') - ->whereNull('categories.deleted_at') - ->get(['categories.id', 'categories.name', 'categories.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $objName = $entry->name; - - // also count the transactions: - $countTransactions = DB::table('category_transaction')->where('category_id', $entry->id)->count(); - - if (0 === $countTransactions) { - $line = sprintf( - 'User #%d (%s) has category #%d ("%s") which has no transactions.', - $entry->user_id, - $entry->email, - $entry->id, - $objName - ); - $this->line($line); - } - } - } - - /** - * Report on journals with bad account types linked to them. - */ - private function reportIncorrectJournals(): void - { - $configuration = [ - // a withdrawal can not have revenue account: - TransactionType::WITHDRAWAL => [AccountType::REVENUE], - // deposit cannot have an expense account: - TransactionType::DEPOSIT => [AccountType::EXPENSE], - // transfer cannot have either: - TransactionType::TRANSFER => [AccountType::EXPENSE, AccountType::REVENUE], - ]; - foreach ($configuration as $transactionType => $accountTypes) { - $set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id') - ->leftJoin('users', 'users.id', '=', 'transaction_journals.user_id') - ->where('transaction_types.type', $transactionType) - ->whereIn('account_types.type', $accountTypes) - ->whereNull('transaction_journals.deleted_at') - ->get( - ['transaction_journals.id', 'transaction_journals.user_id', 'users.email', 'account_types.type as a_type', - 'transaction_types.type',] - ); - foreach ($set as $entry) { - $this->error( - sprintf( - 'Transaction journal #%d (user #%d, %s) is of type "%s" but ' . - 'is linked to a "%s". The transaction journal should be recreated.', - $entry->id, - $entry->user_id, - $entry->email, - $entry->type, - $entry->a_type - ) - ); - } - } - } - - /** - * Any deleted transaction journals that have transactions that are NOT deleted:. - */ - private function reportJournals(): void - { - $count = 0; - $set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNotNull('transaction_journals.deleted_at')// USE THIS - ->whereNull('transactions.deleted_at') - ->whereNotNull('transactions.id') - ->get( - [ - 'transaction_journals.id as journal_id', - 'transaction_journals.description', - 'transaction_journals.deleted_at as journal_deleted', - 'transactions.id as transaction_id', - 'transactions.deleted_at as transaction_deleted_at',] - ); - /** @var stdClass $entry */ - foreach ($set as $entry) { - $this->error( - 'Error: Transaction #' . $entry->transaction_id . ' should have been deleted, but has not.' . - ' Find it in the table called "transactions" and change the "deleted_at" field to: "' . $entry->journal_deleted . '"' - ); - ++$count; - } - if (0 === $count) { - $this->info('No orphaned transactions!'); - } - } - - /** - * Report on journals without transactions. - */ - private function reportNoTransactions(): void - { - $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) { - $this->error( - 'Error: Journal #' . $entry->id . ' has zero transactions. Open table "transaction_journals" and delete the entry with id #' . $entry->id - ); - ++$count; - } - if (0 === $count) { - $this->info('No orphaned journals!'); - } - } - - /** - * Report on things with no linked journals. - * - * @param string $name - */ - private function reportObject(string $name): void - { - $plural = str_plural($name); - $class = sprintf('FireflyIII\Models\%s', ucfirst($name)); - $field = 'tag' === $name ? 'tag' : 'name'; - /** @noinspection PhpUndefinedMethodInspection */ - $set = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id') - ->leftJoin('users', $plural . '.user_id', '=', 'users.id') - ->distinct() - ->whereNull($name . '_transaction_journal.' . $name . '_id') - ->whereNull($plural . '.deleted_at') - ->get([$plural . '.id', $plural . '.' . $field . ' as name', $plural . '.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $objName = $entry->name; - - $line = sprintf( - 'User #%d (%s) has %s #%d ("%s") which has no transactions.', - $entry->user_id, - $entry->email, - $name, - $entry->id, - $objName - ); - $this->line($line); - } - } - - /** - * Reports for each user when the sum of their transactions is not zero. - */ - private function reportSum(): void - { - /** @var UserRepositoryInterface $userRepository */ - $userRepository = app(UserRepositoryInterface::class); - - /** @var User $user */ - foreach ($userRepository->all() as $user) { - $sum = (string)$user->transactions()->sum('amount'); - if (0 !== bccomp($sum, '0')) { - $this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!'); - } - if (0 === bccomp($sum, '0')) { - $this->info(sprintf('Amount integrity OK for user #%d', $user->id)); - } - } - } - - /** - * Reports on deleted transactions that are connected to a not deleted journal. - */ - private function reportTransactions(): void - { - $set = Transaction::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNotNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->get( - ['transactions.id as transaction_id', 'transactions.deleted_at as transaction_deleted', 'transaction_journals.id as journal_id', - 'transaction_journals.deleted_at',] - ); - /** @var stdClass $entry */ - foreach ($set as $entry) { - $this->error( - 'Error: Transaction journal #' . $entry->journal_id . ' should have been deleted, but has not.' . - ' Find it in the table called "transaction_journals" and change the "deleted_at" field to: "' . $entry->transaction_deleted . '"' - ); - } - } - - /** - * Report on transfers that have budgets. - */ - private function reportTransfersBudgets(): void - { - $set = TransactionJournal::distinct() - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id') - ->whereIn('transaction_types.type', [TransactionType::TRANSFER, TransactionType::DEPOSIT]) - ->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']); - - /** @var TransactionJournal $entry */ - foreach ($set as $entry) { - $this->error( - sprintf( - 'Error: Transaction journal #%d is a %s, but has a budget. Edit it without changing anything, so the budget will be removed.', - $entry->id, - $entry->transactionType->type - ) - ); - } - } - - /** - * Collect all journals with empty amount. - */ - private function reportZeroAmount(): void - { - $set = Transaction::where('amount', 0)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); - $set = array_unique($set); - /** @var Collection $journals */ - $journals = TransactionJournal::whereIn('id', $set)->get(); - /** @var TransactionJournal $journal */ - foreach ($journals as $journal) { - $message = sprintf( - 'Transaction "%s" (#%d), owned by user %s, has amount zero (0.00). It should be deleted.', $journal->description, - $journal->id, $journal->user->email - ); - $this->error($message); - } - } - -} diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index fea1a9d7df..564980e431 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -154,26 +154,8 @@ class TransactionFactory Log::debug(sprintf('Now in getAccount(%s)', $direction)); Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true))); // expected type of source account, in order of preference - $array = [ - 'source' => [ - TransactionType::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, - AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION], - TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE], - TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], - ], - 'destination' => [ - TransactionType::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE], - TransactionType::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE], - TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], - ], - ]; + /** @var array $array */ + $array = config('firefly.expected_source_types'); $expectedTypes = $array[$direction]; unset($array); @@ -295,38 +277,7 @@ class TransactionFactory public function makeDramaOverAccountTypes(Account $source, Account $destination): void { // if the source is X, then Y is allowed as destination. - $combinations = [ - TransactionType::WITHDRAWAL => [ - AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], - AccountType::LOAN => [AccountType::EXPENSE], - AccountType::DEBT => [AccountType::EXPENSE], - AccountType::MORTGAGE => [AccountType::EXPENSE], - ], - TransactionType::DEPOSIT => [ - AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::CASH => [AccountType::ASSET], - AccountType::LOAN => [AccountType::ASSET], - AccountType::DEBT => [AccountType::ASSET], - AccountType::MORTGAGE => [AccountType::ASSET], - ], - TransactionType::TRANSFER => [ - AccountType::ASSET => [AccountType::ASSET], - AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - ], - TransactionType::OPENING_BALANCE => [ - AccountType::ASSET => [AccountType::INITIAL_BALANCE], - AccountType::LOAN => [AccountType::INITIAL_BALANCE], - AccountType::DEBT => [AccountType::INITIAL_BALANCE], - AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE], - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - ], - TransactionType::RECONCILIATION => [ - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::ASSET => [AccountType::RECONCILIATION], - ], - ]; + $combinations = config('firefly.source_dests'); $sourceType = $source->accountType->type; $destType = $destination->accountType->type; $journalType = $this->journal->transactionType->type; diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 5f26fcbc71..aed1042db5 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -129,7 +129,9 @@ class TransactionJournalFactory Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions))); /** Get basic fields */ - $currency = $this->currencyRepository->findCurrency($transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code']); + $currency = $this->currencyRepository->findCurrency( + $transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code'] + ); $foreignCurrency = $this->findForeignCurrency($transaction); $bill = $this->billRepository->findBill($transaction['bill'], (int)$transaction['bill_id'], $transaction['bill_name']); @@ -189,9 +191,8 @@ class TransactionJournalFactory $collection->push($journal); } - if ($collection->count() > 1) { - $this->storeGroup($collection, $data['group_title']); - } + + $this->storeGroup($collection, $data['group_title']); return $collection; diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index dea02540e1..5cc205d8f0 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -26,6 +26,7 @@ use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -103,11 +104,11 @@ class TransactionGroup extends Model /** * @codeCoverageIgnore - * @return BelongsToMany + * @return HasMany */ - public function transactionJournals(): BelongsToMany + public function transactionJournals(): HasMany { - return $this->belongsToMany(TransactionJournal::class,'group_journals'); + return $this->hasMany(TransactionJournal::class); } /** diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index c667421dfd..bde4a81de8 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -39,43 +39,45 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class TransactionJournal. * - * @property User $user - * @property int $bill_id - * @property Collection $categories - * @property bool $completed - * @property string $description - * @property int $transaction_type_id - * @property int transaction_currency_id - * @property TransactionCurrency $transactionCurrency - * @property Collection $tags - * @property int user_id - * @property Collection transactions - * @property int transaction_count - * @property Carbon interest_date - * @property Carbon book_date - * @property Carbon process_date - * @property bool encrypted - * @property int order - * @property int budget_id - * @property string period_marker - * @property Carbon $date - * @property string $transaction_type_type - * @property int $id - * @property TransactionType $transactionType - * @property Collection budgets - * @property Bill $bill - * @property Collection transactionJournalMeta + * @property User $user + * @property int $bill_id + * @property Collection $categories + * @property bool $completed + * @property string $description + * @property int $transaction_type_id + * @property int transaction_currency_id + * @property TransactionCurrency $transactionCurrency + * @property Collection $tags + * @property int user_id + * @property Collection transactions + * @property int transaction_count + * @property Carbon interest_date + * @property Carbon book_date + * @property Carbon process_date + * @property bool encrypted + * @property int order + * @property int budget_id + * @property string period_marker + * @property Carbon $date + * @property string $transaction_type_type + * @property int $id + * @property TransactionType $transactionType + * @property Collection budgets + * @property Bill $bill + * @property Collection transactionJournalMeta + * @property TransactionGroup transactionGroup + * @property int transaction_group_id * @SuppressWarnings (PHPMD.TooManyPublicMethods) * @SuppressWarnings (PHPMD.CouplingBetweenObjects) - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at - * @property \Illuminate\Support\Carbon|null $deleted_at - * @property int $tag_count - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property \Illuminate\Support\Carbon|null $deleted_at + * @property int $tag_count + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalLink[] $sourceJournalLinks - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $transactionGroups + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $transactionGroups * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\TransactionJournal after(\Carbon\Carbon $date) * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\TransactionJournal before(\Carbon\Carbon $date) * @method static bool|null forceDelete() @@ -362,11 +364,11 @@ class TransactionJournal extends Model /** * @codeCoverageIgnore - * @return BelongsToMany + * @return BelongsTo */ - public function transactionGroups(): BelongsToMany + public function transactionGroup(): BelongsToMany { - return $this->belongsToMany(TransactionGroup::class,'group_journals'); + return $this->belongsTo(TransactionGroup::class); } /** diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 9daf96f5bf..982e1d9866 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -482,6 +482,16 @@ class JournalRepository implements JournalRepositoryInterface return $amount; } + /** + * Return all journals without a group, used in an upgrade routine. + * + * @return Collection + */ + public function getJournalsWithoutGroup(): Collection + { + return TransactionJournal::whereNull('transaction_group_id')->get(); + } + /** * @param TransactionJournalLink $link * diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index c511acf8a7..60cdb8f3d7 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -39,7 +39,6 @@ use Illuminate\Support\MessageBag; */ interface JournalRepositoryInterface { - /** * @param TransactionJournal $journal * @param TransactionType $type @@ -57,9 +56,6 @@ interface JournalRepositoryInterface */ public function countTransactions(TransactionJournal $journal): int; - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * Deletes a journal. * @@ -69,6 +65,9 @@ interface JournalRepositoryInterface */ public function destroy(TransactionJournal $journal): bool; + + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * Find a journal by its hash. * @@ -205,6 +204,13 @@ interface JournalRepositoryInterface */ public function getJournalTotal(TransactionJournal $journal): string; + /** + * Return all journals without a group, used in an upgrade routine. + * + * @return Collection + */ + public function getJournalsWithoutGroup(): Collection; + /** * @param TransactionJournalLink $link * diff --git a/config/firefly.php b/config/firefly.php index 8e9e3ba4da..6559323c28 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -24,6 +24,8 @@ declare(strict_types=1); use FireflyIII\Export\Exporter\CsvExporter; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\TransactionType as TransactionTypeModel; use FireflyIII\Services\Currency\FixerIOv2; use FireflyIII\Services\Currency\RatesApiIOv1; use FireflyIII\TransactionRules\Actions\AddTag; @@ -278,7 +280,6 @@ return [ 'zh_CN' => ['name_locale' => 'Chinese Simplified', 'name_english' => 'Chinese Simplified'], // 99% - //'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'], // 71% 'nb_NO' => ['name_locale' => 'Norsk', 'name_english' => 'Norwegian'], //'ca_ES' => ['name_locale' => 'Catalan', 'name_english' => 'Catalan'], // 0% @@ -474,17 +475,73 @@ return [ ], - 'test-triggers' => [ + 'test-triggers' => [ 'limit' => 10, 'range' => 200, ], - 'default_currency' => 'EUR', - 'default_language' => 'en_US', - 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', - 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'], + 'default_currency' => 'EUR', + 'default_language' => 'en_US', + 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', + 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'], // tag notes has_attachments - 'cer_providers' => [ + 'cer_providers' => [ 'fixer' => FixerIOv2::class, 'ratesapi' => RatesApiIOv1::class, ], + + // expected source types for each transaction type, in order of preference. + 'expected_source_types' => [ + 'source' => [ + TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, + AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION], + TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE], + TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + ], + 'destination' => [ + TransactionTypeModel::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE], + TransactionTypeModel::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE], + TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + ], + ], + + // allowed source / destination accounts. + 'source_dests' => [ + TransactionTypeModel::WITHDRAWAL => [ + AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], + AccountType::LOAN => [AccountType::EXPENSE], + AccountType::DEBT => [AccountType::EXPENSE], + AccountType::MORTGAGE => [AccountType::EXPENSE], + ], + TransactionTypeModel::DEPOSIT => [ + AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::CASH => [AccountType::ASSET], + AccountType::LOAN => [AccountType::ASSET], + AccountType::DEBT => [AccountType::ASSET], + AccountType::MORTGAGE => [AccountType::ASSET], + ], + TransactionTypeModel::TRANSFER => [ + AccountType::ASSET => [AccountType::ASSET], + AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + ], + TransactionTypeModel::OPENING_BALANCE => [ + AccountType::ASSET => [AccountType::INITIAL_BALANCE], + AccountType::LOAN => [AccountType::INITIAL_BALANCE], + AccountType::DEBT => [AccountType::INITIAL_BALANCE], + AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE], + AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + ], + TransactionTypeModel::RECONCILIATION => [ + AccountType::RECONCILIATION => [AccountType::ASSET], + AccountType::ASSET => [AccountType::RECONCILIATION], + ], + ], ]; diff --git a/database/migrations/2019_03_03_081750_changes_for_v480.php b/database/migrations/2019_03_03_081750_changes_for_v480.php deleted file mode 100644 index 0294c1d307..0000000000 --- a/database/migrations/2019_03_03_081750_changes_for_v480.php +++ /dev/null @@ -1,26 +0,0 @@ -dropForeign('transaction_journals_transaction_group_id_foreign'); + } + $table->dropColumn('transaction_group_id'); + } + ); + } + + /** + * Run the migrations. + * + * @return void + */ + public function up(): void + { + + Schema::table( + 'transaction_journals', + function (Blueprint $table) { + + $table->integer('transaction_currency_id', false, true)->nullable()->change(); + + // add column "group_id" after "transaction_type_id" + $table->integer('transaction_group_id', false, true) + ->nullable()->default(null)->after('transaction_type_id'); + + // add foreign key for "transaction_group_id" + $table->foreign('transaction_group_id')->references('id')->on('transaction_groups')->onDelete('cascade'); + } + ); + } +}