2018-07-06 00:15:42 -05:00
< ? php
2016-05-20 04:59:54 -05:00
/**
* VerifyDatabase . php
2018-05-11 03:08:34 -05:00
* Copyright ( c ) 2018 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 .
2016-10-04 23:52:15 -05:00
*
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
2017-12-17 07:41:58 -06:00
* along with Firefly III . If not , see < http :// www . gnu . org / licenses />.
2016-05-20 04:59:54 -05:00
*/
2016-04-24 11:25:52 -05:00
2018-07-06 00:15:42 -05:00
/** @noinspection PhpDynamicAsStaticMethodCallInspection */
2018-05-11 03:08:34 -05:00
declare ( strict_types = 1 );
2016-04-24 11:25:52 -05:00
namespace FireflyIII\Console\Commands ;
use Crypt ;
2017-12-10 04:52:14 -06:00
use DB ;
2016-04-24 11:25:52 -05:00
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 ;
2018-07-04 22:53:21 -05:00
use FireflyIII\Models\Category ;
2017-09-09 11:36:17 -05:00
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 ;
2016-09-23 15:31:01 -05:00
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 ;
2018-07-06 00:15:42 -05:00
use Log ;
2017-09-14 11:27:22 -05:00
use Preferences ;
2016-11-07 13:25:09 -06:00
use Schema ;
2016-04-24 11:25:52 -05:00
use stdClass ;
/**
2017-11-15 05:25:49 -06:00
* Class VerifyDatabase .
2018-07-06 00:15:42 -05:00
*
* @ SuppressWarnings ( PHPMD . ExcessiveClassComplexity )
* @ 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' ;
/**
* Execute the console command .
*/
2018-07-05 14:18:53 -05:00
public function handle () : void
2016-04-24 11:25:52 -05:00
{
2016-11-07 13:25:09 -06:00
// if table does not exist, return false
if ( ! Schema :: hasTable ( 'users' )) {
return ;
}
2018-07-04 22:53:21 -05:00
$this -> reportEmptyBudgets ();
$this -> reportEmptyCategories ();
2016-11-26 03:39:05 -06:00
$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 ();
2016-09-23 15:31:01 -05:00
$this -> reportTransfersBudgets ();
2016-10-26 12:32:07 -05:00
$this -> reportIncorrectJournals ();
2017-08-13 05:30:28 -05:00
$this -> repairPiggyBanks ();
2017-09-09 11:36:17 -05:00
$this -> createLinkTypes ();
2017-09-14 11:27:22 -05:00
$this -> createAccessTokens ();
2017-12-10 04:52:14 -06:00
$this -> fixDoubleAmounts ();
2018-04-14 02:59:04 -05:00
$this -> fixBadMeta ();
2018-07-03 10:48:26 -05:00
$this -> removeBills ();
2017-09-14 11:27:22 -05:00
}
2017-09-14 14:17:19 -05:00
/**
2017-09-16 00:41:03 -05:00
* Create user access tokens , if not present already .
2017-09-14 14:17:19 -05:00
*/
2018-04-14 02:59:04 -05:00
private function createAccessTokens () : void
2017-09-14 11:27:22 -05:00
{
2017-12-10 04:52:14 -06:00
$count = 0 ;
2017-09-14 11:27:22 -05:00
$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 ) {
2017-09-14 11:27:22 -05:00
$token = $user -> generateAccessToken ();
Preferences :: setForUser ( $user , 'access_token' , $token );
2017-09-14 14:17:19 -05:00
$this -> line ( sprintf ( 'Generated access token for user %s' , $user -> email ));
2017-12-22 11:32:43 -06:00
++ $count ;
2017-09-14 11:27:22 -05:00
}
}
2017-12-22 11:32:43 -06:00
if ( 0 === $count ) {
2017-12-10 04:52:14 -06:00
$this -> info ( 'All access tokens OK!' );
}
2017-09-09 11:36:17 -05:00
}
/**
2017-09-16 00:41:03 -05:00
* Create default link types if necessary .
2017-09-09 11:36:17 -05:00
*/
2018-04-14 02:59:04 -05:00
private function createLinkTypes () : void
2017-09-09 11:36:17 -05:00
{
2017-12-10 04:52:14 -06:00
$count = 0 ;
$set = [
2017-09-27 07:50:04 -05:00
'Related' => [ 'relates to' , 'relates to' ],
2017-09-09 11:36:17 -05:00
'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 ) {
2017-09-09 11:36:17 -05:00
$link = new LinkType ;
$link -> name = $name ;
$link -> outward = $values [ 0 ];
$link -> inward = $values [ 1 ];
2017-12-22 11:32:43 -06:00
++ $count ;
2017-09-09 11:36:17 -05:00
}
$link -> editable = false ;
$link -> save ();
}
2017-12-22 11:32:43 -06:00
if ( 0 === $count ) {
2017-12-10 04:52:14 -06:00
$this -> info ( 'All link types OK!' );
}
}
2018-04-14 02:59:04 -05:00
/**
2018-07-06 00:15:42 -05:00
* Fix the situation where the matching transactions of a journal somehow have non - matching categories or budgets .
*
* @ SuppressWarnings ( PHPMD . CyclomaticComplexity )
* @ SuppressWarnings ( PHPMD . ExcessiveMethodLength )
2018-04-14 02:59:04 -05:00
*/
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 (
2018-06-06 14:23:00 -05:00
'Transaction #%d referred to the wrong category. Was category #%d but is fixed to be category #%d.' , $obj -> transaction_journal_id ,
$category , $results [ $key ]
2018-04-14 02:59:04 -05:00
)
);
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 ) {
2018-06-06 14:23:00 -05:00
$key = $obj -> transaction_journal_id . '-' . $obj -> identifier ;
2018-04-14 02:59:04 -05:00
$budget = ( int ) $obj -> budget_id ;
// value exists and is not budget:
if ( isset ( $results [ $key ]) && $results [ $key ] !== $budget ) {
$this -> error (
sprintf (
2018-06-06 14:23:00 -05:00
'Transaction #%d referred to the wrong budget. Was budget #%d but is fixed to be budget #%d.' , $obj -> transaction_journal_id , $budget ,
$results [ $key ]
2018-04-14 02:59:04 -05:00
)
);
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 ;
}
}
}
2018-07-06 00:15:42 -05:00
/**
* Makes sure amounts are stored correctly .
*
* @ SuppressWarnings ( PHPMD . CyclomaticComplexity )
* @ SuppressWarnings ( PHPMD . ExcessiveMethodLength )
*/
2018-04-14 02:59:04 -05:00
private function fixDoubleAmounts () : void
2017-12-10 04:52:14 -06:00
{
$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 ) {
2018-04-02 07:42:07 -05:00
if ( 0 !== bccomp (( string ) $entry -> the_sum , '0' )) {
2017-12-10 04:52:14 -06:00
$errored [] = $entry -> transaction_journal_id ;
}
}
foreach ( $errored as $journalId ) {
// select and update:
2017-12-11 03:59:28 -06:00
$res = Transaction :: whereNull ( 'deleted_at' ) -> where ( 'transaction_journal_id' , $journalId ) -> groupBy ( 'amount' ) -> get ([ DB :: raw ( 'MIN(id) as first_id' )]);
2017-12-10 04:52:14 -06:00
$ids = $res -> pluck ( 'first_id' ) -> toArray ();
DB :: table ( 'transactions' ) -> whereIn ( 'id' , $ids ) -> update ([ 'amount' => DB :: raw ( 'amount * -1' )]);
2017-12-22 11:32:43 -06:00
++ $count ;
2017-12-10 04:52:14 -06:00
// report about it
/** @var TransactionJournal $journal */
$journal = TransactionJournal :: find ( $journalId );
2018-04-02 07:42:07 -05:00
if ( null === $journal ) {
2017-12-11 03:59:28 -06:00
continue ;
}
2017-12-22 11:32:43 -06:00
if ( TransactionType :: OPENING_BALANCE === $journal -> transactionType -> type ) {
2017-12-10 04:52:14 -06:00
$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
)
);
}
2017-12-22 11:32:43 -06:00
if ( TransactionType :: OPENING_BALANCE !== $journal -> transactionType -> type ) {
2017-12-10 04:52:14 -06:00
$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
)
);
}
}
2017-12-22 11:32:43 -06:00
if ( 0 === $count ) {
2017-12-10 04:52:14 -06:00
$this -> info ( 'Amount integrity OK!' );
}
2017-08-13 05:30:28 -05:00
}
2018-07-03 10:48:26 -05:00
/**
2018-07-06 00:15:42 -05:00
* Removes bills from journals that should not have bills .
2018-07-03 10:48:26 -05:00
*/
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 ();
}
}
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
{
2017-12-11 03:59:28 -06:00
$set = PiggyBankEvent :: with ([ 'PiggyBank' , 'TransactionJournal' , 'TransactionJournal.TransactionType' ]) -> get ();
2017-08-13 05:30:28 -05:00
$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 ;
}
);
2016-04-24 11:25:52 -05:00
}
/**
* Reports on accounts with no transactions .
*/
2018-07-05 14:18:53 -05:00
private function reportAccounts () : void
2016-04-24 11:25:52 -05:00
{
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 ) {
2016-08-24 12:36:09 -05:00
$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 ) .
*/
2018-04-14 02:59:04 -05:00
private function reportBudgetLimits () : void
2016-04-24 11:25:52 -05:00
{
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' )
2016-12-25 04:50:42 -06:00
-> groupBy ([ 'budgets.id' , 'budgets.name' , 'budgets.encrypted' , 'budgets.user_id' , 'users.email' ])
2016-12-14 11:59:12 -06:00
-> whereNull ( 'budget_limits.id' )
2016-12-25 04:50:42 -06:00
-> get ([ 'budgets.id' , 'budgets.name' , 'budgets.user_id' , 'budgets.encrypted' , 'users.email' ]);
2016-04-24 11:25:52 -05:00
2016-12-25 04:50:42 -06:00
/** @var Budget $entry */
2016-04-24 11:25:52 -05:00
foreach ( $set as $entry ) {
2016-11-02 01:04:14 -05:00
$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-11-02 01:04:14 -05:00
);
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 .
*/
2018-04-14 02:59:04 -05:00
private function reportDeletedAccounts () : void
2016-04-24 11:25:52 -05:00
{
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 ) {
2018-04-02 07:42:07 -05:00
$date = $entry -> transaction_deleted_at ? ? $entry -> journal_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
}
2018-07-04 22:53:21 -05:00
/**
* 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 ;
try {
$objName = Crypt :: decrypt ( $objName );
} catch ( DecryptException $e ) {
// it probably was not encrypted.
2018-07-06 00:15:42 -05:00
Log :: debug ( sprintf ( 'Not a problem: %s' , $e -> getMessage ()));
2018-07-04 22:53:21 -05:00
}
// also count the transactions:
$countTransactions = DB :: table ( 'budget_transaction' ) -> where ( 'budget_id' , $entry -> id ) -> count ();
2018-07-05 14:18:53 -05:00
if ( 0 === $countTransactions ) {
2018-07-04 22:53:21 -05:00
$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' )
2018-07-05 14:18:53 -05:00
-> 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' ]);
2018-07-04 22:53:21 -05:00
/** @var stdClass $entry */
foreach ( $set as $entry ) {
$objName = $entry -> name ;
try {
$objName = Crypt :: decrypt ( $objName );
} catch ( DecryptException $e ) {
// it probably was not encrypted.
2018-07-06 00:15:42 -05:00
Log :: debug ( sprintf ( 'Not a problem: %s' , $e -> getMessage ()));
2018-07-04 22:53:21 -05:00
}
// also count the transactions:
$countTransactions = DB :: table ( 'category_transaction' ) -> where ( 'category_id' , $entry -> id ) -> count ();
2018-07-05 14:18:53 -05:00
if ( 0 === $countTransactions ) {
2018-07-04 22:53:21 -05:00
$line = sprintf (
'User #%d (%s) has category #%d ("%s") which has no transactions.' ,
$entry -> user_id ,
$entry -> email ,
$entry -> id ,
$objName
);
$this -> line ( $line );
}
}
}
2017-08-15 10:26:43 -05:00
/**
* Report on journals with bad account types linked to them .
*/
2018-04-14 02:59:04 -05:00
private function reportIncorrectJournals () : void
2016-10-26 12:32:07 -05:00
{
$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
*/
2018-04-14 02:59:04 -05:00
private function reportJournals () : void
2016-04-24 11:25:52 -05:00
{
2017-12-10 04:52:14 -06:00
$count = 0 ;
2017-12-11 03:59:28 -06:00
$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' ,]
);
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-12-22 11:32:43 -06:00
++ $count ;
2017-12-10 04:52:14 -06:00
}
2017-12-22 11:32:43 -06:00
if ( 0 === $count ) {
2017-12-10 04:52:14 -06:00
$this -> info ( 'No orphaned transactions!' );
2016-04-24 11:25:52 -05:00
}
}
2016-09-23 15:31:01 -05:00
/**
2017-08-15 10:26:43 -05:00
* Report on journals without transactions .
2016-09-23 15:31:01 -05:00
*/
2018-04-14 02:59:04 -05:00
private function reportNoTransactions () : void
2016-04-29 10:26:38 -05:00
{
2017-12-10 04:52:14 -06:00
$count = 0 ;
2017-12-11 03:59:28 -06:00
$set = TransactionJournal :: leftJoin ( 'transactions' , 'transactions.transaction_journal_id' , '=' , 'transaction_journals.id' )
-> 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
);
2017-12-22 11:32:43 -06:00
++ $count ;
2017-12-10 04:52:14 -06:00
}
2017-12-22 11:32:43 -06:00
if ( 0 === $count ) {
2017-12-10 04:52:14 -06:00
$this -> info ( 'No orphaned journals!' );
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
*/
2018-04-14 02:59:04 -05:00
private function reportObject ( string $name ) : void
2016-11-26 03:39:05 -06:00
{
$plural = str_plural ( $name );
$class = sprintf ( 'FireflyIII\Models\%s' , ucfirst ( $name ));
2017-11-15 05:25:49 -06:00
$field = 'tag' === $name ? 'tag' : 'name' ;
2018-07-05 14:18:53 -05:00
/** @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' ]);
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.
2018-07-06 00:15:42 -05:00
Log :: debug ( sprintf ( 'Not a problem: %s' , $e -> getMessage ()));
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 .
*/
2018-04-14 02:59:04 -05:00
private function reportSum () : void
2016-04-24 11:25:52 -05:00
{
/** @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 ) {
2018-04-02 07:42:07 -05:00
$sum = ( string ) $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 . '!' );
2018-07-06 00:15:42 -05:00
}
if ( 0 === bccomp ( $sum , '0' )) {
2017-12-10 04:52:14 -06:00
$this -> info ( sprintf ( 'Amount integrity OK for user #%d' , $user -> id ));
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 .
*/
2018-04-14 02:59:04 -05:00
private function reportTransactions () : void
2016-04-24 11:25:52 -05:00
{
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
}
2016-09-23 15:31:01 -05:00
/**
2017-08-15 10:26:43 -05:00
* Report on transfers that have budgets .
2016-09-23 15:31:01 -05:00
*/
2018-07-04 22:53:21 -05:00
private function reportTransfersBudgets () : void
2016-09-23 15:31:01 -05:00
{
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.*' ]);
2016-09-23 15:31:01 -05:00
/** @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-09-23 15:31:01 -05:00
)
);
}
}
2016-04-24 11:25:52 -05:00
}