mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-01-24 15:26:33 -06:00
350 lines
14 KiB
PHP
350 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* VerifyDatabase.php
|
|
* Copyright (C) 2016 thegrumpydictator@gmail.com
|
|
*
|
|
* This software may be modified and distributed under the terms of the
|
|
* Creative Commons Attribution-ShareAlike 4.0 International License.
|
|
*
|
|
* See the LICENSE file for details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace FireflyIII\Console\Commands;
|
|
|
|
use Crypt;
|
|
use FireflyIII\Models\Account;
|
|
use FireflyIII\Models\AccountType;
|
|
use FireflyIII\Models\Budget;
|
|
use FireflyIII\Models\Transaction;
|
|
use FireflyIII\Models\TransactionJournal;
|
|
use FireflyIII\Models\TransactionType;
|
|
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
|
use FireflyIII\User;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Contracts\Encryption\DecryptException;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Schema;
|
|
use stdClass;
|
|
|
|
/**
|
|
* Class VerifyDatabase
|
|
*
|
|
* @package FireflyIII\Console\Commands
|
|
*/
|
|
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;
|
|
}
|
|
|
|
$this->reportObject('budget');
|
|
$this->reportObject('category');
|
|
$this->reportObject('tag');
|
|
|
|
// accounts with no transactions.
|
|
$this->reportAccounts();
|
|
// budgets with no limits
|
|
$this->reportBudgetLimits();
|
|
// budgets with no transactions
|
|
|
|
// sum of transactions is not zero.
|
|
$this->reportSum();
|
|
// any deleted transaction journals that have transactions that are NOT deleted:
|
|
$this->reportJournals();
|
|
// deleted transactions that are connected to a not deleted journal.
|
|
$this->reportTransactions();
|
|
// deleted accounts that still have not deleted transactions or journals attached to them.
|
|
$this->reportDeletedAccounts();
|
|
|
|
// report on journals with no transactions at all.
|
|
$this->reportNoTransactions();
|
|
|
|
// transfers with budgets.
|
|
$this->reportTransfersBudgets();
|
|
|
|
// report on journals with the wrong types of accounts.
|
|
$this->reportIncorrectJournals();
|
|
|
|
}
|
|
|
|
/**
|
|
* Reports on accounts with no transactions.
|
|
*/
|
|
private function reportAccounts()
|
|
{
|
|
$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()
|
|
{
|
|
$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()
|
|
{
|
|
$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 = is_null($entry->transaction_deleted_at) ? $entry->journal_deleted_at : $entry->transaction_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 . '"'
|
|
);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
$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()
|
|
{
|
|
$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 . '"'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
private function reportNoTransactions()
|
|
{
|
|
$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
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
*/
|
|
private function reportObject(string $name)
|
|
{
|
|
$plural = str_plural($name);
|
|
$class = sprintf('FireflyIII\Models\%s', ucfirst($name));
|
|
$field = $name == 'tag' ? 'tag' : 'name';
|
|
$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;
|
|
try {
|
|
$objName = Crypt::decrypt($objName);
|
|
} catch (DecryptException $e) {
|
|
// it probably was not encrypted.
|
|
}
|
|
|
|
$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()
|
|
{
|
|
/** @var UserRepositoryInterface $userRepository */
|
|
$userRepository = app(UserRepositoryInterface::class);
|
|
|
|
/** @var User $user */
|
|
foreach ($userRepository->all() as $user) {
|
|
$sum = strval($user->transactions()->sum('amount'));
|
|
if (bccomp($sum, '0') !== 0) {
|
|
$this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reports on deleted transactions that are connected to a not deleted journal.
|
|
*/
|
|
private function reportTransactions()
|
|
{
|
|
$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 . '"'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
private function reportTransfersBudgets()
|
|
{
|
|
$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')
|
|
->where('transaction_types.type', TransactionType::TRANSFER)
|
|
->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.id']);
|
|
|
|
/** @var TransactionJournal $entry */
|
|
foreach ($set as $entry) {
|
|
$this->error(
|
|
sprintf(
|
|
'Error: Transaction journal #%d is a transfer, but has a budget. Edit it without changing anything, so the budget will be removed.',
|
|
$entry->id
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
}
|
|
}
|