firefly-iii/app/Console/Commands/VerifyDatabase.php

430 lines
17 KiB
PHP
Raw Normal View History

2016-04-24 11:25:52 -05:00
<?php
2016-05-20 04:59:54 -05:00
/**
* VerifyDatabase.php
2017-10-21 01:40:00 -05:00
* Copyright (c) 2017 thegrumpydictator@gmail.com
2016-05-20 04:59:54 -05:00
*
2017-10-21 01:40:00 -05:00
* This file is part of Firefly III.
*
2017-10-21 01:40:00 -05:00
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
2016-05-20 04:59:54 -05:00
*/
declare(strict_types=1);
2016-04-24 11:25:52 -05:00
namespace FireflyIII\Console\Commands;
use Crypt;
use FireflyIII\Models\Account;
2016-10-26 12:32:07 -05:00
use FireflyIII\Models\AccountType;
2016-04-24 11:25:52 -05:00
use FireflyIII\Models\Budget;
use FireflyIII\Models\LinkType;
2017-08-13 05:30:28 -05:00
use FireflyIII\Models\PiggyBankEvent;
2016-04-24 11:35:45 -05:00
use FireflyIII\Models\Transaction;
2016-04-24 11:25:52 -05:00
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
2016-04-24 11:25:52 -05:00
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Console\Command;
2016-11-26 03:39:05 -06:00
use Illuminate\Contracts\Encryption\DecryptException;
2016-04-24 11:35:45 -05:00
use Illuminate\Database\Eloquent\Builder;
use Preferences;
use Schema;
2016-04-24 11:25:52 -05:00
use stdClass;
/**
2017-11-15 05:25:49 -06:00
* Class VerifyDatabase.
2016-04-24 11:25:52 -05:00
*
2017-09-16 00:41:03 -05:00
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2016-04-24 11:25:52 -05:00
*/
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';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
// if table does not exist, return false
if (!Schema::hasTable('users')) {
return;
}
2016-11-26 03:39:05 -06:00
$this->reportObject('budget');
$this->reportObject('category');
$this->reportObject('tag');
2016-04-24 11:25:52 -05:00
$this->reportAccounts();
$this->reportBudgetLimits();
$this->reportSum();
$this->reportJournals();
$this->reportTransactions();
$this->reportDeletedAccounts();
2016-04-29 10:26:38 -05:00
$this->reportNoTransactions();
$this->reportTransfersBudgets();
2016-10-26 12:32:07 -05:00
$this->reportIncorrectJournals();
2017-08-13 05:30:28 -05:00
$this->repairPiggyBanks();
$this->createLinkTypes();
$this->createAccessTokens();
}
/**
2017-09-16 00:41:03 -05:00
* Create user access tokens, if not present already.
*/
private function createAccessTokens()
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$pref = Preferences::getForUser($user, 'access_token', null);
2017-11-15 05:25:49 -06:00
if (null === $pref) {
$token = $user->generateAccessToken();
Preferences::setForUser($user, 'access_token', $token);
$this->line(sprintf('Generated access token for user %s', $user->email));
}
}
}
/**
2017-09-16 00:41:03 -05:00
* Create default link types if necessary.
*/
private function createLinkTypes()
{
$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();
2017-11-15 05:25:49 -06:00
if (null === $link) {
$link = new LinkType;
$link->name = $name;
$link->outward = $values[0];
$link->inward = $values[1];
}
$link->editable = false;
$link->save();
}
2017-08-13 05:30:28 -05:00
}
/**
2017-09-16 00:41:03 -05:00
* Eeport (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events.
2017-08-13 05:30:28 -05:00
*/
private function repairPiggyBanks(): void
{
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
$set->each(
function (PiggyBankEvent $event) {
2017-11-15 05:25:49 -06:00
if (null === $event->transaction_journal_id) {
2017-08-13 05:30:28 -05:00
return true;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
2017-11-15 05:25:49 -06:00
if (null === $journal) {
2017-08-13 05:30:28 -05:00
return true;
}
$type = $journal->transactionType->type;
2017-11-15 05:25:49 -06:00
if (TransactionType::TRANSFER !== $type) {
2017-08-13 05:30:28 -05:00
$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;
}
);
return;
2016-04-24 11:25:52 -05:00
}
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts()
{
2016-11-28 13:38:03 -06:00
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
2016-12-14 11:59:12 -06:00
->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']
);
2016-04-24 11:25:52 -05:00
/** @var stdClass $entry */
foreach ($set as $entry) {
$name = $entry->name;
2016-08-13 16:31:42 -05:00
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name);
2016-04-24 11:25:52 -05:00
$this->line($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits()
{
2016-11-28 13:38:03 -06:00
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
2016-12-14 11:59:12 -06:00
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
2016-12-14 11:59:12 -06:00
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
2016-04-24 11:25:52 -05:00
/** @var Budget $entry */
2016-04-24 11:25:52 -05:00
foreach ($set as $entry) {
$line = sprintf(
2017-04-08 00:00:51 -05:00
'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
2017-11-15 03:50:23 -06:00
$entry->user_id,
$entry->email,
$entry->id,
$entry->name
);
2016-04-24 11:25:52 -05:00
$this->line($line);
}
}
2016-04-24 11:35:45 -05:00
/**
* Reports on deleted accounts that still have not deleted transactions or journals attached to them.
*/
2016-04-24 11:25:52 -05:00
private function reportDeletedAccounts()
{
2016-11-28 13:38:03 -06:00
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
2016-12-14 11:59:12 -06:00
->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',
2017-11-15 05:25:49 -06:00
'transaction_journals.deleted_at as journal_deleted_at',]
2016-12-14 11:59:12 -06:00
);
2016-04-24 11:35:45 -05:00
/** @var stdClass $entry */
foreach ($set as $entry) {
2017-11-15 05:25:49 -06:00
$date = null === $entry->transaction_deleted_at ? $entry->journal_deleted_at : $entry->transaction_deleted_at;
2016-04-24 11:35:45 -05:00
$this->error(
2016-04-24 11:36:44 -05:00
'Error: Account #' . $entry->account_id . ' should have been deleted, but has not.' .
2016-10-05 22:26:38 -05:00
' Find it in the table called "accounts" and change the "deleted_at" field to: "' . $date . '"'
2016-04-24 11:35:45 -05:00
);
}
2016-04-24 11:25:52 -05:00
}
2017-08-15 10:26:43 -05:00
/**
* Report on journals with bad account types linked to them.
*/
2016-10-26 12:32:07 -05:00
private function reportIncorrectJournals()
{
$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) {
2016-11-28 13:38:03 -06:00
$set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
2016-12-14 11:59:12 -06:00
->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',
2017-11-15 05:25:49 -06:00
'transaction_types.type',]
2016-12-14 11:59:12 -06:00
);
2016-10-26 12:32:07 -05:00
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
)
);
}
}
}
2016-04-24 11:25:52 -05:00
/**
2017-11-15 05:25:49 -06:00
* Any deleted transaction journals that have transactions that are NOT deleted:.
2016-04-24 11:25:52 -05:00
*/
private function reportJournals()
{
2016-11-28 13:38:03 -06:00
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
2016-12-14 11:59:12 -06:00
->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',
2017-11-15 05:25:49 -06:00
'transactions.deleted_at as transaction_deleted_at',]
2016-12-14 11:59:12 -06:00
);
2016-04-24 11:25:52 -05:00
/** @var stdClass $entry */
foreach ($set as $entry) {
$this->error(
2016-04-24 11:36:44 -05:00
'Error: Transaction #' . $entry->transaction_id . ' should have been deleted, but has not.' .
2016-10-05 22:26:38 -05:00
' Find it in the table called "transactions" and change the "deleted_at" field to: "' . $entry->journal_deleted . '"'
2016-04-24 11:25:52 -05:00
);
}
}
/**
2017-08-15 10:26:43 -05:00
* Report on journals without transactions.
*/
2016-04-29 10:26:38 -05:00
private function reportNoTransactions()
{
2016-11-28 13:38:03 -06:00
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
2016-12-14 11:59:12 -06:00
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id']);
2016-04-29 10:26:38 -05:00
foreach ($set as $entry) {
$this->error(
2016-10-05 22:26:38 -05:00
'Error: Journal #' . $entry->id . ' has zero transactions. Open table "transaction_journals" and delete the entry with id #' . $entry->id
2016-04-29 10:26:38 -05:00
);
}
}
2016-11-26 03:39:05 -06:00
/**
2017-08-15 10:26:43 -05:00
* Report on things with no linked journals.
2017-09-08 23:41:45 -05:00
*
2016-11-26 03:39:05 -06:00
* @param string $name
*/
private function reportObject(string $name)
{
$plural = str_plural($name);
$class = sprintf('FireflyIII\Models\%s', ucfirst($name));
2017-11-15 05:25:49 -06:00
$field = 'tag' === $name ? 'tag' : 'name';
2016-11-26 06:02:44 -06:00
$set = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id')
2016-12-14 11:59:12 -06:00
->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']);
2016-11-26 03:39:05 -06:00
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
try {
$objName = Crypt::decrypt($objName);
} catch (DecryptException $e) {
2016-11-26 06:02:44 -06:00
// it probably was not encrypted.
2016-11-26 03:39:05 -06:00
}
$line = sprintf(
2017-04-08 00:00:51 -05:00
'User #%d (%s) has %s #%d ("%s") which has no transactions.',
2017-11-15 03:50:23 -06:00
$entry->user_id,
$entry->email,
$name,
$entry->id,
$objName
2016-11-26 03:39:05 -06:00
);
$this->line($line);
}
}
2016-04-24 11:25:52 -05:00
/**
* Reports for each user when the sum of their transactions is not zero.
*/
private function reportSum()
{
/** @var UserRepositoryInterface $userRepository */
2016-05-01 08:05:29 -05:00
$userRepository = app(UserRepositoryInterface::class);
2016-04-24 11:25:52 -05:00
/** @var User $user */
foreach ($userRepository->all() as $user) {
$sum = strval($user->transactions()->sum('amount'));
2017-11-15 05:25:49 -06:00
if (0 !== bccomp($sum, '0')) {
2016-04-24 11:36:44 -05:00
$this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!');
2016-04-24 11:25:52 -05:00
}
}
}
2016-04-24 11:35:45 -05:00
/**
* Reports on deleted transactions that are connected to a not deleted journal.
*/
2016-04-24 11:25:52 -05:00
private function reportTransactions()
{
2016-11-26 06:02:44 -06:00
$set = Transaction::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
2016-12-14 11:59:12 -06:00
->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',
2017-11-15 05:25:49 -06:00
'transaction_journals.deleted_at',]
2016-12-14 11:59:12 -06:00
);
2016-04-24 11:35:45 -05:00
/** @var stdClass $entry */
foreach ($set as $entry) {
$this->error(
2016-04-24 11:36:44 -05:00
'Error: Transaction journal #' . $entry->journal_id . ' should have been deleted, but has not.' .
2016-10-05 22:26:38 -05:00
' Find it in the table called "transaction_journals" and change the "deleted_at" field to: "' . $entry->transaction_deleted . '"'
2016-04-24 11:35:45 -05:00
);
}
2016-04-24 11:25:52 -05:00
}
/**
2017-08-15 10:26:43 -05:00
* Report on transfers that have budgets.
*/
private function reportTransfersBudgets()
{
2016-11-28 13:52:56 -06:00
$set = TransactionJournal::distinct()
2016-12-14 11:59:12 -06:00
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id')
2017-10-13 10:32:14 -05:00
->whereIn('transaction_types.type', [TransactionType::TRANSFER, TransactionType::DEPOSIT])
2017-10-13 10:34:19 -05:00
->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']);
/** @var TransactionJournal $entry */
foreach ($set as $entry) {
$this->error(
sprintf(
2017-10-13 10:32:14 -05:00
'Error: Transaction journal #%d is a %s, but has a budget. Edit it without changing anything, so the budget will be removed.',
2017-11-15 03:50:23 -06:00
$entry->id,
$entry->transactionType->type
)
);
}
}
2016-04-24 11:25:52 -05:00
}