2020-04-19 12:29:58 +02:00
< ? php
2020-09-07 12:18:56 +02:00
/**
* Invoice Ninja ( https :// invoiceninja . com ) .
*
* @ link https :// github . com / invoiceninja / invoiceninja source repository
*
2021-01-03 22:54:54 +01:00
* @ copyright Copyright ( c ) 2021. Invoice Ninja LLC ( https :// invoiceninja . com )
2020-09-07 12:18:56 +02:00
*
* @ license https :// opensource . org / licenses / AAL
*/
2020-04-19 12:29:58 +02:00
namespace App\Console\Commands ;
use App ;
use App\Models\Account ;
2020-05-26 14:37:15 +02:00
use App\Models\Client ;
2020-04-19 12:29:58 +02:00
use App\Models\ClientContact ;
2020-05-26 14:37:15 +02:00
use App\Models\CompanyLedger ;
2020-04-19 12:29:58 +02:00
use App\Models\Contact ;
2020-10-18 11:25:32 +02:00
use App\Models\Credit ;
2020-04-19 12:29:58 +02:00
use App\Models\Invoice ;
use App\Models\InvoiceInvitation ;
2020-11-25 10:21:26 +01:00
use App\Models\Payment ;
2020-04-19 12:29:58 +02:00
use App\Utils\Ninja ;
use DB ;
use Exception ;
use Illuminate\Console\Command ;
use Mail ;
use Symfony\Component\Console\Input\InputOption ;
/*
##################################################################
WARNING : Please backup your database before running this script
##################################################################
If you have any questions please email us at contact @ invoiceninja . com
Usage :
php artisan ninja : check - data
Options :
-- client_id :< value >
Limits the script to a single client
-- fix = true
By default the script only checks for errors , adding this option
makes the script apply the fixes .
-- fast = true
Skip using phantomjs
*/
/**
* Class CheckData .
*/
class CheckData extends Command
{
/**
* @ var string
*/
protected $name = 'ninja:check-data' ;
/**
* @ var string
*/
protected $description = 'Check/fix data' ;
protected $log = '' ;
protected $isValid = true ;
public function handle ()
{
2020-09-06 11:38:10 +02:00
$this -> logMessage ( date ( 'Y-m-d h:i:s' ) . ' Running CheckData...' );
2020-04-19 12:29:58 +02:00
if ( $database = $this -> option ( 'database' )) {
config ([ 'database.default' => $database ]);
}
$this -> checkInvoiceBalances ();
2020-07-02 06:23:30 +02:00
$this -> checkInvoicePayments ();
2020-07-02 07:25:34 +02:00
$this -> checkPaidToDates ();
2020-04-19 12:29:58 +02:00
$this -> checkClientBalances ();
$this -> checkContacts ();
2020-06-28 00:24:08 +02:00
$this -> checkCompanyData ();
2020-04-19 12:29:58 +02:00
//$this->checkLogoFiles();
if ( ! $this -> option ( 'client_id' )) {
$this -> checkOAuth ();
//$this->checkInvitations();
$this -> checkFailedJobs ();
}
2020-09-06 11:38:10 +02:00
$this -> logMessage ( 'Done: ' . strtoupper ( $this -> isValid ? Account :: RESULT_SUCCESS : Account :: RESULT_FAILURE ));
2020-04-19 12:29:58 +02:00
$errorEmail = config ( 'ninja.error_email' );
if ( $errorEmail ) {
Mail :: raw ( $this -> log , function ( $message ) use ( $errorEmail , $database ) {
$message -> to ( $errorEmail )
-> from ( config ( 'ninja.error_email' ))
2020-09-06 11:38:10 +02:00
-> subject ( 'Check-Data: ' . strtoupper ( $this -> isValid ? Account :: RESULT_SUCCESS : Account :: RESULT_FAILURE ) . " [ { $database } ] " );
2020-04-19 12:29:58 +02:00
});
} elseif ( ! $this -> isValid ) {
2020-10-21 05:10:32 +02:00
new Exception ( " Check data failed!! \n " . $this -> log );
2020-04-19 12:29:58 +02:00
}
}
private function logMessage ( $str )
{
2020-09-06 11:38:10 +02:00
$str = date ( 'Y-m-d h:i:s' ) . ' ' . $str ;
2020-04-19 12:29:58 +02:00
$this -> info ( $str );
2020-09-06 11:38:10 +02:00
$this -> log .= $str . " \n " ;
2020-04-19 12:29:58 +02:00
}
private function checkOAuth ()
{
// check for duplicate oauth ids
$users = DB :: table ( 'users' )
-> whereNotNull ( 'oauth_user_id' )
-> groupBy ( 'users.oauth_user_id' )
-> havingRaw ( 'count(users.id) > 1' )
-> get ([ 'users.oauth_user_id' ]);
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $users -> count () . ' users with duplicate oauth ids' );
2020-04-19 12:29:58 +02:00
if ( $users -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $users as $user ) {
$first = true ;
2020-09-06 11:38:10 +02:00
$this -> logMessage ( 'checking ' . $user -> oauth_user_id );
2020-04-19 12:29:58 +02:00
$matches = DB :: table ( 'users' )
-> where ( 'oauth_user_id' , '=' , $user -> oauth_user_id )
-> orderBy ( 'id' )
-> get ([ 'id' ]);
foreach ( $matches as $match ) {
if ( $first ) {
2020-09-06 11:38:10 +02:00
$this -> logMessage ( 'skipping ' . $match -> id );
2020-04-19 12:29:58 +02:00
$first = false ;
continue ;
}
2020-09-06 11:38:10 +02:00
$this -> logMessage ( 'updating ' . $match -> id );
2020-04-19 12:29:58 +02:00
DB :: table ( 'users' )
-> where ( 'id' , '=' , $match -> id )
-> where ( 'oauth_user_id' , '=' , $user -> oauth_user_id )
-> update ([
'oauth_user_id' => null ,
'oauth_provider_id' => null ,
]);
}
}
}
}
private function checkContacts ()
{
// check for contacts with the contact_key value set
$contacts = DB :: table ( 'client_contacts' )
-> whereNull ( 'contact_key' )
-> orderBy ( 'id' )
-> get ([ 'id' ]);
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $contacts -> count () . ' contacts without a contact_key' );
2020-04-19 12:29:58 +02:00
if ( $contacts -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $contacts as $contact ) {
DB :: table ( 'client_contacts' )
-> where ( 'id' , '=' , $contact -> id )
-> whereNull ( 'contact_key' )
-> update ([
'contact_key' => str_random ( config ( 'ninja.key_length' )),
]);
}
}
// check for missing contacts
$clients = DB :: table ( 'clients' )
2020-09-06 11:38:10 +02:00
-> leftJoin ( 'client_contacts' , function ( $join ) {
2020-04-19 12:29:58 +02:00
$join -> on ( 'client_contacts.client_id' , '=' , 'clients.id' )
-> whereNull ( 'client_contacts.deleted_at' );
})
-> groupBy ( 'clients.id' , 'clients.user_id' , 'clients.company_id' )
-> havingRaw ( 'count(client_contacts.id) = 0' );
if ( $this -> option ( 'client_id' )) {
$clients -> where ( 'clients.id' , '=' , $this -> option ( 'client_id' ));
}
$clients = $clients -> get ([ 'clients.id' , 'clients.user_id' , 'clients.company_id' ]);
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $clients -> count () . ' clients without any contacts' );
2020-04-19 12:29:58 +02:00
if ( $clients -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $clients as $client ) {
$contact = new ClientContact ();
$contact -> company_id = $client -> company_id ;
$contact -> user_id = $client -> user_id ;
$contact -> client_id = $client -> id ;
$contact -> is_primary = true ;
$contact -> send_invoice = true ;
$contact -> contact_key = str_random ( config ( 'ninja.key_length' ));
$contact -> save ();
}
}
// check for more than one primary contact
$clients = DB :: table ( 'clients' )
2020-09-06 11:38:10 +02:00
-> leftJoin ( 'client_contacts' , function ( $join ) {
2020-04-19 12:29:58 +02:00
$join -> on ( 'client_contacts.client_id' , '=' , 'clients.id' )
-> where ( 'client_contacts.is_primary' , '=' , true )
-> whereNull ( 'client_contacts.deleted_at' );
})
-> groupBy ( 'clients.id' )
-> havingRaw ( 'count(client_contacts.id) != 1' );
if ( $this -> option ( 'client_id' )) {
$clients -> where ( 'clients.id' , '=' , $this -> option ( 'client_id' ));
}
$clients = $clients -> get ([ 'clients.id' , DB :: raw ( 'count(client_contacts.id)' )]);
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $clients -> count () . ' clients without a single primary contact' );
2020-04-19 12:29:58 +02:00
if ( $clients -> count () > 0 ) {
$this -> isValid = false ;
}
}
private function checkFailedJobs ()
{
if ( config ( 'ninja.testvars.travis' )) {
return ;
}
$queueDB = config ( 'queue.connections.database.connection' );
$count = DB :: connection ( $queueDB ) -> table ( 'failed_jobs' ) -> count ();
if ( $count > 25 ) {
$this -> isValid = false ;
}
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $count . ' failed jobs' );
2020-04-19 12:29:58 +02:00
}
private function checkInvitations ()
{
$invoices = DB :: table ( 'invoices' )
-> leftJoin ( 'invoice_invitations' , function ( $join ) {
$join -> on ( 'invoice_invitations.invoice_id' , '=' , 'invoices.id' )
-> whereNull ( 'invoice_invitations.deleted_at' );
})
-> groupBy ( 'invoices.id' , 'invoices.user_id' , 'invoices.company_id' , 'invoices.client_id' )
-> havingRaw ( 'count(invoice_invitations.id) = 0' )
-> get ([ 'invoices.id' , 'invoices.user_id' , 'invoices.company_id' , 'invoices.client_id' ]);
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $invoices -> count () . ' invoices without any invitations' );
2020-04-19 12:29:58 +02:00
if ( $invoices -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $invoices as $invoice ) {
$invitation = new InvoiceInvitation ();
$invitation -> company_id = $invoice -> company_id ;
$invitation -> user_id = $invoice -> user_id ;
$invitation -> invoice_id = $invoice -> id ;
$invitation -> contact_id = ClientContact :: whereClientId ( $invoice -> client_id ) -> whereIsPrimary ( true ) -> first () -> id ;
$invitation -> invitation_key = str_random ( config ( 'ninja.key_length' ));
$invitation -> save ();
}
}
}
2020-07-02 07:25:34 +02:00
private function checkPaidToDates ()
{
$wrong_paid_to_dates = 0 ;
2020-10-18 11:25:32 +02:00
$credit_total_applied = 0 ;
2020-07-02 07:25:34 +02:00
2020-12-02 09:59:45 +01:00
Client :: withTrashed () -> where ( 'is_deleted' , 0 ) -> cursor () -> each ( function ( $client ) use ( $wrong_paid_to_dates , $credit_total_applied ) {
2020-09-06 11:38:10 +02:00
$total_invoice_payments = 0 ;
2020-07-02 07:25:34 +02:00
2020-11-19 04:52:22 +01:00
foreach ( $client -> invoices -> where ( 'is_deleted' , false ) -> where ( 'status_id' , '>' , 1 ) as $invoice ) {
2020-11-25 10:21:26 +01:00
// $total_amount = $invoice->payments->whereNull('deleted_at')->sum('pivot.amount');
// $total_refund = $invoice->payments->whereNull('deleted_at')->sum('pivot.refunded');
2021-01-11 11:45:38 +01:00
$total_amount = $invoice -> payments -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ]) -> sum ( 'pivot.amount' );
$total_refund = $invoice -> payments -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ]) -> sum ( 'pivot.refunded' );
2020-09-06 11:38:10 +02:00
2020-11-25 15:19:52 +01:00
$total_invoice_payments += ( $total_amount - $total_refund );
2020-07-05 12:16:12 +02:00
}
2020-09-06 11:38:10 +02:00
2020-11-25 15:19:52 +01:00
foreach ( $client -> payments as $payment ) {
$credit_total_applied += $payment -> paymentables -> where ( 'paymentable_type' , App\Models\Credit :: class ) -> sum ( DB :: raw ( 'amount' ));
2020-10-18 11:25:32 +02:00
}
2020-11-25 15:19:52 +01:00
if ( $credit_total_applied < 0 ) {
$total_invoice_payments += $credit_total_applied ;
} //todo this is contentious
2020-10-18 11:25:32 +02:00
2020-12-29 22:10:03 +01:00
nlog ( " total invoice payments = { $total_invoice_payments } with client paid to date of of { $client -> paid_to_date } " );
2020-10-18 11:25:32 +02:00
2020-09-06 11:38:10 +02:00
if ( round ( $total_invoice_payments , 2 ) != round ( $client -> paid_to_date , 2 )) {
2020-07-02 07:25:34 +02:00
$wrong_paid_to_dates ++ ;
2020-11-18 10:35:09 +01:00
$this -> logMessage ( $client -> present () -> name . 'id = # ' . $client -> id . " - Paid to date does not match Client Paid To Date = { $client -> paid_to_date } - Invoice Payments = { $total_invoice_payments } " );
2020-07-02 07:25:34 +02:00
$this -> isValid = false ;
}
});
$this -> logMessage ( " { $wrong_paid_to_dates } clients with incorrect paid to dates " );
}
2020-07-02 06:23:30 +02:00
private function checkInvoicePayments ()
{
$wrong_balances = 0 ;
2020-07-02 07:25:34 +02:00
$wrong_paid_to_dates = 0 ;
2020-07-02 06:23:30 +02:00
2020-12-02 09:59:45 +01:00
Client :: cursor () -> where ( 'is_deleted' , 0 ) -> each ( function ( $client ) use ( $wrong_balances ) {
2020-11-15 09:24:57 +01:00
$client -> invoices -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , '!=' , Invoice :: STATUS_DRAFT ) -> each ( function ( $invoice ) use ( $wrong_balances , $client ) {
2020-11-25 10:21:26 +01:00
$total_amount = $invoice -> payments -> whereIn ( 'status_id' , [ Payment :: STATUS_PAID , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED ]) -> sum ( 'pivot.amount' );
$total_refund = $invoice -> payments -> whereIn ( 'status_id' , [ Payment :: STATUS_PAID , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED ]) -> sum ( 'pivot.refunded' );
2020-07-16 05:54:26 +02:00
$total_credit = $invoice -> credits -> sum ( 'amount' );
2020-07-02 06:23:30 +02:00
$total_paid = $total_amount - $total_refund ;
2020-10-18 09:46:10 +02:00
$calculated_paid_amount = $invoice -> amount - $invoice -> balance - $total_credit ;
2020-07-02 06:23:30 +02:00
2020-10-18 09:46:10 +02:00
if (( string ) $total_paid != ( string )( $invoice -> amount - $invoice -> balance - $total_credit )) {
2020-07-02 06:23:30 +02:00
$wrong_balances ++ ;
2020-10-18 09:46:10 +02:00
$this -> logMessage ( $client -> present () -> name . ' - ' . $client -> id . " - Total Amount = { $total_amount } != Calculated Total = { $calculated_paid_amount } - Total Refund = { $total_refund } Total credit = { $total_credit } " );
2020-07-02 06:23:30 +02:00
$this -> isValid = false ;
}
});
});
2020-05-26 14:37:15 +02:00
2020-09-06 11:38:10 +02:00
$this -> logMessage ( " { $wrong_balances } clients with incorrect invoice balances " );
2020-04-19 12:29:58 +02:00
}
private function checkClientBalances ()
{
2020-05-26 14:37:15 +02:00
$wrong_balances = 0 ;
$wrong_paid_to_dates = 0 ;
2020-12-02 09:59:45 +01:00
foreach ( Client :: cursor () -> where ( 'is_deleted' , 0 ) as $client ) {
2020-11-23 13:55:04 +01:00
//$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$invoice_balance = Invoice :: where ( 'client_id' , $client -> id ) -> where ( 'is_deleted' , false ) -> where ( 'status_id' , '>' , 1 ) -> withTrashed () -> sum ( 'balance' );
2021-01-21 02:33:39 +01:00
$credit_balance = Credit :: where ( 'client_id' , $client -> id ) -> where ( 'is_deleted' , false ) -> withTrashed () -> sum ( 'balance' );
2020-11-24 11:12:05 +01:00
2021-02-03 13:29:44 +01:00
/*Legacy - V4 will add credits to the balance - we may need to reverse engineer this and remove the credits from the client balance otherwise we need this hack here and in the invoice balance check.*/
if ( $client -> balance != $invoice_balance )
$invoice_balance -= $credit_balance ;
2020-07-16 05:54:26 +02:00
2020-05-26 14:37:15 +02:00
$ledger = CompanyLedger :: where ( 'client_id' , $client -> id ) -> orderBy ( 'id' , 'DESC' ) -> first ();
2020-10-18 11:25:32 +02:00
if ( $ledger && ( string ) $invoice_balance != ( string ) $client -> balance ) {
2020-05-26 14:37:15 +02:00
$wrong_paid_to_dates ++ ;
2020-11-19 06:16:47 +01:00
$this -> logMessage ( $client -> present () -> name . ' - ' . $client -> id . " - calculated client balances do not match { $invoice_balance } - " . rtrim ( $client -> balance , '0' ));
2020-05-26 14:37:15 +02:00
2020-09-06 11:38:10 +02:00
$this -> isValid = false ;
2020-05-26 14:37:15 +02:00
}
2020-09-06 11:38:10 +02:00
}
2020-05-26 14:37:15 +02:00
2020-11-23 13:55:04 +01:00
$this -> logMessage ( " { $wrong_paid_to_dates } clients with incorrect client balances " );
2020-04-19 12:29:58 +02:00
}
2021-02-03 13:29:44 +01:00
private function checkInvoiceBalances ()
{
$wrong_balances = 0 ;
$wrong_paid_to_dates = 0 ;
foreach ( Client :: where ( 'is_deleted' , 0 ) -> cursor () as $client ) {
$invoice_balance = $client -> invoices -> where ( 'is_deleted' , false ) -> where ( 'status_id' , '>' , 1 ) -> sum ( 'balance' );
$credit_balance = $client -> credits -> where ( 'is_deleted' , false ) -> sum ( 'balance' );
if ( $client -> balance != $invoice_balance )
$invoice_balance -= $credit_balance ; //doesn't make sense to remove the credit amount
$ledger = CompanyLedger :: where ( 'client_id' , $client -> id ) -> orderBy ( 'id' , 'DESC' ) -> first ();
if ( $ledger && number_format ( $invoice_balance , 4 ) != number_format ( $client -> balance , 4 )) {
$wrong_balances ++ ;
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " - Balance Failure - Invoice Balances = { $invoice_balance } Client Balance = { $client -> balance } Ledger Balance = { $ledger -> balance } " );
$this -> isValid = false ;
}
}
$this -> logMessage ( " { $wrong_balances } clients with incorrect balances " );
}
2020-04-19 12:29:58 +02:00
private function checkLogoFiles ()
{
// $accounts = DB::table('accounts')
// ->where('logo', '!=', '')
// ->orderBy('id')
// ->get(['logo']);
// $countMissing = 0;
// foreach ($accounts as $account) {
// $path = public_path('logo/' . $account->logo);
// if (! file_exists($path)) {
// $this->logMessage('Missing file: ' . $account->logo);
// $countMissing++;
// }
// }
// if ($countMissing > 0) {
// $this->isValid = false;
// }
// $this->logMessage($countMissing . ' missing logo files');
}
/**
* @ return array
*/
protected function getArguments ()
{
return [];
}
/**
* @ return array
*/
protected function getOptions ()
{
return [
[ 'fix' , null , InputOption :: VALUE_OPTIONAL , 'Fix data' , null ],
[ 'fast' , null , InputOption :: VALUE_OPTIONAL , 'Fast' , null ],
[ 'client_id' , null , InputOption :: VALUE_OPTIONAL , 'Client id' , null ],
[ 'database' , null , InputOption :: VALUE_OPTIONAL , 'Database' , null ],
];
}
2020-06-28 00:24:08 +02:00
private function checkCompanyData ()
{
$tables = [
'activities' => [
'invoice' ,
'client' ,
'client_contact' ,
'payment' ,
],
'invoices' => [
'client' ,
],
'payments' => [
'client' ,
],
'products' => [
],
];
foreach ( $tables as $table => $entityTypes ) {
foreach ( $entityTypes as $entityType ) {
$tableName = $this -> pluralizeEntityType ( $entityType );
$field = $entityType ;
if ( $table == 'companies' ) {
$company_id = 'id' ;
} else {
$company_id = 'company_id' ;
}
2020-10-28 11:10:49 +01:00
$records = DB :: table ( $table )
2020-06-28 00:24:08 +02:00
-> join ( $tableName , " { $tableName } .id " , '=' , " { $table } . { $field } _id " )
2020-10-28 11:10:49 +01:00
-> where ( " { $table } . { $company_id } " , '!=' , DB :: raw ( " { $tableName } .company_id " ))
2020-06-28 00:24:08 +02:00
-> get ([ " { $table } .id " ]);
if ( $records -> count ()) {
$this -> isValid = false ;
2020-09-06 11:38:10 +02:00
$this -> logMessage ( $records -> count () . " { $table } records with incorrect { $entityType } company id " );
2020-06-28 00:24:08 +02:00
}
}
}
// foreach(User::cursor() as $user) {
// $records = Company::where('account_id',)
// }
}
public function pluralizeEntityType ( $type )
{
if ( $type === 'company' ) {
return 'companies' ;
2020-09-06 11:38:10 +02:00
}
2020-06-28 00:24:08 +02:00
2020-09-06 11:38:10 +02:00
return $type . 's' ;
2020-06-28 00:24:08 +02:00
}
2020-04-19 12:29:58 +02:00
}