2017-01-30 20:40:43 +01:00
< ? php
namespace App\Console\Commands ;
2015-03-16 22:45:25 +01:00
2015-05-05 11:48:23 +02:00
use Carbon ;
2017-07-03 12:15:06 +02:00
use App\Libraries\CurlUtils ;
2017-01-30 20:40:43 +01:00
use DB ;
2017-03-09 16:23:56 +01:00
use Exception ;
2015-03-16 22:45:25 +01:00
use Illuminate\Console\Command ;
2017-01-30 20:40:43 +01:00
use Mail ;
2015-03-16 22:45:25 +01:00
use Symfony\Component\Console\Input\InputOption ;
2017-01-30 20:40:43 +01:00
use Utils ;
2017-04-24 15:41:36 +02:00
use App\Models\Contact ;
2017-05-28 16:12:29 +02:00
use App\Models\Invoice ;
2017-04-30 15:09:17 +02:00
use App\Models\Invitation ;
2015-03-16 22:45:25 +01:00
/*
##################################################################
2016-08-25 16:29:55 +02:00
WARNING : Please backup your database before running this script
2015-03-16 22:45:25 +01:00
##################################################################
2016-08-25 16:29:55 +02:00
Since the application was released a number of bugs have inevitably been found .
2015-03-16 22:45:25 +01:00
Although the bugs have always been fixed in some cases they 've caused the client' s
balance , paid to date and / or activity records to become inaccurate . This script will
check for errors and correct the data .
If you have any questions please email us at contact @ invoiceninja . com
Usage :
php artisan ninja : check - data
Options :
2016-08-25 16:29:55 +02:00
-- client_id :< value >
2015-03-16 22:45:25 +01:00
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 .
2017-11-07 09:40:46 +01:00
-- fast = true
Skip using phantomjs
2015-03-16 22:45:25 +01:00
*/
2016-07-03 18:11:58 +02:00
/**
2017-01-30 20:40:43 +01:00
* Class CheckData .
2016-07-03 18:11:58 +02:00
*/
2017-01-30 17:05:31 +01:00
class CheckData extends Command
{
2016-07-03 18:11:58 +02:00
/**
* @ var string
*/
2015-03-16 22:45:25 +01:00
protected $name = 'ninja:check-data' ;
2016-07-03 18:11:58 +02:00
/**
* @ var string
*/
2015-03-16 22:45:25 +01:00
protected $description = 'Check/fix data' ;
2016-08-25 16:29:55 +02:00
2016-09-19 10:54:01 +02:00
protected $log = '' ;
protected $isValid = true ;
2015-03-16 22:45:25 +01:00
public function fire ()
{
2017-07-25 20:48:36 +02:00
$this -> logMessage ( date ( 'Y-m-d h:i:s' ) . ' Running CheckData...' );
2015-03-16 22:45:25 +01:00
2017-05-01 14:17:52 +02:00
if ( $database = $this -> option ( 'database' )) {
config ([ 'database.default' => $database ]);
}
2017-01-30 20:40:43 +01:00
if ( ! $this -> option ( 'client_id' )) {
2016-08-29 17:03:08 +02:00
$this -> checkBlankInvoiceHistory ();
2017-04-30 15:09:17 +02:00
$this -> checkPaidToDate ();
2017-05-28 16:12:29 +02:00
$this -> checkDraftSentInvoices ();
2015-12-02 14:26:06 +01:00
}
2017-07-12 22:34:57 +02:00
$this -> checkInvoices ();
2017-10-21 23:38:31 +02:00
$this -> checkInvoiceBalances ();
$this -> checkClientBalances ();
2017-04-24 15:41:36 +02:00
$this -> checkContacts ();
2017-05-28 12:19:48 +02:00
$this -> checkUserAccounts ();
2015-12-02 14:26:06 +01:00
2017-01-30 20:40:43 +01:00
if ( ! $this -> option ( 'client_id' )) {
2017-05-15 15:32:49 +02:00
$this -> checkOAuth ();
2017-04-30 15:09:17 +02:00
$this -> checkInvitations ();
2016-08-29 17:03:08 +02:00
$this -> checkAccountData ();
2017-05-11 11:46:26 +02:00
$this -> checkLookupData ();
2017-07-31 11:48:30 +02:00
$this -> checkFailedJobs ();
2016-08-29 17:03:08 +02:00
}
2015-12-02 14:26:06 +01:00
2017-05-11 11:46:26 +02:00
$this -> logMessage ( 'Done: ' . strtoupper ( $this -> isValid ? RESULT_SUCCESS : RESULT_FAILURE ));
2016-09-19 10:54:01 +02:00
$errorEmail = env ( 'ERROR_EMAIL' );
2017-01-10 10:51:21 +01:00
if ( $errorEmail ) {
2017-05-01 17:35:06 +02:00
Mail :: raw ( $this -> log , function ( $message ) use ( $errorEmail , $database ) {
2017-01-02 14:00:02 +01:00
$message -> to ( $errorEmail )
-> from ( CONTACT_EMAIL )
2017-10-18 19:13:38 +02:00
-> subject ( " Check-Data: " . strtoupper ( $this -> isValid ? RESULT_SUCCESS : RESULT_FAILURE ) . " [ { $database } ] " );
2017-01-02 14:00:02 +01:00
});
2017-03-09 16:23:56 +01:00
} elseif ( ! $this -> isValid ) {
throw new Exception ( 'Check data failed!!' );
2016-09-19 10:54:01 +02:00
}
}
private function logMessage ( $str )
{
2017-05-30 12:03:06 +02:00
$str = date ( 'Y-m-d h:i:s' ) . ' ' . $str ;
2017-05-28 16:12:29 +02:00
$this -> info ( $str );
2016-09-19 10:54:01 +02:00
$this -> log .= $str . " \n " ;
2015-12-02 14:26:06 +01:00
}
2017-05-28 16:12:29 +02:00
private function checkDraftSentInvoices ()
{
$invoices = Invoice :: whereInvoiceStatusId ( INVOICE_STATUS_SENT )
-> whereIsPublic ( false )
-> withTrashed ()
-> get ();
$this -> logMessage ( count ( $invoices ) . ' draft sent invoices' );
2017-06-14 11:42:33 +02:00
if ( count ( $invoices ) > 0 ) {
$this -> isValid = false ;
}
2017-05-28 16:12:29 +02:00
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $invoices as $invoice ) {
2017-08-02 20:26:56 +02:00
$dispatcher = $invoice -> getEventDispatcher ();
2017-05-28 16:12:29 +02:00
if ( $invoice -> is_deleted ) {
$invoice -> unsetEventDispatcher ();
}
2017-08-02 20:26:56 +02:00
$invoice -> is_public = true ;
$invoice -> save ();
$invoice -> markInvitationsSent ();
$invoice -> setEventDispatcher ( $dispatcher );
2017-05-28 16:12:29 +02:00
}
}
}
2017-07-03 12:15:06 +02:00
private function checkInvoices ()
{
2017-08-08 20:03:49 +02:00
if ( ! env ( 'PHANTOMJS_BIN_PATH' ) || ! Utils :: isNinjaProd ()) {
2017-07-03 12:15:06 +02:00
return ;
}
2017-11-07 09:40:46 +01:00
if ( $this -> option ( 'fix' ) == 'true' || $this -> option ( 'fast' ) == 'true' ) {
2017-07-27 17:29:23 +02:00
return ;
}
2017-08-06 15:28:00 +02:00
$isValid = true ;
2017-07-03 12:15:06 +02:00
$date = new Carbon ();
$date = $date -> subDays ( 1 ) -> format ( 'Y-m-d' );
2017-07-03 14:09:55 +02:00
$invoices = Invoice :: with ( 'invitations' )
2017-07-03 12:15:06 +02:00
-> where ( 'created_at' , '>' , $date )
-> orderBy ( 'id' )
2017-08-09 13:31:57 +02:00
-> get ();
2017-07-03 12:15:06 +02:00
foreach ( $invoices as $invoice ) {
2017-07-12 22:34:57 +02:00
$link = $invoice -> getInvitationLink ( 'view' , true , true );
2017-07-03 12:15:06 +02:00
$result = CurlUtils :: phantom ( 'GET' , $link . '?phantomjs=true&phantomjs_balances=true&phantomjs_secret=' . env ( 'PHANTOMJS_SECRET' ));
$result = floatval ( strip_tags ( $result ));
2017-08-17 10:56:46 +02:00
$invoice = $invoice -> fresh ();
2017-08-09 13:31:57 +02:00
//$this->logMessage('Checking invoice: ' . $invoice->id . ' - ' . $invoice->balance);
2017-08-06 15:08:30 +02:00
//$this->logMessage('Result: ' . $result);
2017-07-03 12:15:06 +02:00
2017-07-03 14:09:55 +02:00
if ( $result && $result != $invoice -> balance ) {
2017-08-10 18:05:21 +02:00
$this -> logMessage ( " PHP/JS amounts do not match { $link } ?silent=true | PHP: { $invoice -> balance } , JS: { $result } " );
2017-08-06 15:28:00 +02:00
$this -> isValid = $isValid = false ;
2017-07-03 12:15:06 +02:00
}
}
2017-08-06 15:28:00 +02:00
if ( $isValid ) {
2017-07-03 12:15:06 +02:00
$this -> logMessage ( '0 invoices with mismatched PHP/JS balances' );
}
}
2017-05-15 15:32:49 +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' ]);
$this -> logMessage ( count ( $users ) . ' users with duplicate oauth ids' );
if ( count ( $users ) > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $users as $user ) {
$first = true ;
$this -> logMessage ( 'checking ' . $user -> oauth_user_id );
$matches = DB :: table ( 'users' )
-> where ( 'oauth_user_id' , '=' , $user -> oauth_user_id )
-> orderBy ( 'id' )
-> get ([ 'id' ]);
foreach ( $matches as $match ) {
if ( $first ) {
$this -> logMessage ( 'skipping ' . $match -> id );
$first = false ;
continue ;
}
$this -> logMessage ( 'updating ' . $match -> id );
DB :: table ( 'users' )
-> where ( 'id' , '=' , $match -> id )
-> where ( 'oauth_user_id' , '=' , $user -> oauth_user_id )
-> update ([
'oauth_user_id' => null ,
'oauth_provider_id' => null ,
]);
}
}
}
}
2017-05-11 11:46:26 +02:00
private function checkLookupData ()
{
$tables = [
'account_tokens' ,
'accounts' ,
'companies' ,
'contacts' ,
'invitations' ,
'users' ,
];
foreach ( $tables as $table ) {
$count = DB :: table ( 'lookup_' . $table ) -> count ();
if ( $count > 0 ) {
$this -> logMessage ( " Lookup table { $table } has { $count } records " );
$this -> isValid = false ;
}
}
}
2017-05-04 13:00:43 +02:00
private function checkUserAccounts ()
{
$userAccounts = DB :: table ( 'user_accounts' )
-> leftJoin ( 'users as u1' , 'u1.id' , '=' , 'user_accounts.user_id1' )
-> leftJoin ( 'accounts as a1' , 'a1.id' , '=' , 'u1.account_id' )
-> leftJoin ( 'users as u2' , 'u2.id' , '=' , 'user_accounts.user_id2' )
-> leftJoin ( 'accounts as a2' , 'a2.id' , '=' , 'u2.account_id' )
-> leftJoin ( 'users as u3' , 'u3.id' , '=' , 'user_accounts.user_id3' )
-> leftJoin ( 'accounts as a3' , 'a3.id' , '=' , 'u3.account_id' )
-> leftJoin ( 'users as u4' , 'u4.id' , '=' , 'user_accounts.user_id4' )
-> leftJoin ( 'accounts as a4' , 'a4.id' , '=' , 'u4.account_id' )
-> leftJoin ( 'users as u5' , 'u5.id' , '=' , 'user_accounts.user_id5' )
-> leftJoin ( 'accounts as a5' , 'a5.id' , '=' , 'u5.account_id' )
-> get ([
'user_accounts.id' ,
'a1.company_id as a1_company_id' ,
'a2.company_id as a2_company_id' ,
'a3.company_id as a3_company_id' ,
'a4.company_id as a4_company_id' ,
'a5.company_id as a5_company_id' ,
]);
$countInvalid = 0 ;
foreach ( $userAccounts as $userAccount ) {
$ids = [];
if ( $companyId1 = $userAccount -> a1_company_id ) {
$ids [ $companyId1 ] = true ;
}
if ( $companyId2 = $userAccount -> a2_company_id ) {
$ids [ $companyId2 ] = true ;
}
if ( $companyId3 = $userAccount -> a3_company_id ) {
$ids [ $companyId3 ] = true ;
}
if ( $companyId4 = $userAccount -> a4_company_id ) {
$ids [ $companyId4 ] = true ;
}
if ( $companyId5 = $userAccount -> a5_company_id ) {
$ids [ $companyId5 ] = true ;
}
if ( count ( $ids ) > 1 ) {
$this -> info ( 'user_account: ' . $userAccount -> id );
$countInvalid ++ ;
}
}
2017-05-28 12:19:48 +02:00
$this -> logMessage ( $countInvalid . ' user accounts with multiple companies' );
2017-05-04 13:00:43 +02:00
if ( $countInvalid > 0 ) {
$this -> isValid = false ;
}
}
2017-04-24 15:41:36 +02:00
private function checkContacts ()
{
2017-05-03 18:26:03 +02:00
// check for contacts with the contact_key value set
$contacts = DB :: table ( 'contacts' )
-> whereNull ( 'contact_key' )
-> orderBy ( 'id' )
-> get ([ 'id' ]);
$this -> logMessage ( count ( $contacts ) . ' contacts without a contact_key' );
if ( count ( $contacts ) > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $contacts as $contact ) {
DB :: table ( 'contacts' )
2017-05-15 15:32:49 +02:00
-> where ( 'id' , '=' , $contact -> id )
2017-05-03 18:26:03 +02:00
-> whereNull ( 'contact_key' )
-> update ([
'contact_key' => strtolower ( str_random ( RANDOM_KEY_LENGTH )),
]);
}
}
// check for missing contacts
2017-04-24 15:41:36 +02:00
$clients = DB :: table ( 'clients' )
-> leftJoin ( 'contacts' , function ( $join ) {
$join -> on ( 'contacts.client_id' , '=' , 'clients.id' )
-> whereNull ( 'contacts.deleted_at' );
})
-> groupBy ( 'clients.id' , 'clients.user_id' , 'clients.account_id' )
-> havingRaw ( 'count(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.account_id' ]);
$this -> logMessage ( count ( $clients ) . ' clients without any contacts' );
if ( count ( $clients ) > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $clients as $client ) {
$contact = new Contact ();
$contact -> account_id = $client -> account_id ;
$contact -> user_id = $client -> user_id ;
$contact -> client_id = $client -> id ;
$contact -> is_primary = true ;
$contact -> send_invoice = true ;
$contact -> contact_key = strtolower ( str_random ( RANDOM_KEY_LENGTH ));
$contact -> public_id = Contact :: whereAccountId ( $client -> account_id ) -> withTrashed () -> max ( 'public_id' ) + 1 ;
$contact -> save ();
}
}
2017-05-03 18:26:03 +02:00
// check for more than one primary contact
2017-04-24 15:41:36 +02:00
$clients = DB :: table ( 'clients' )
-> leftJoin ( 'contacts' , function ( $join ) {
$join -> on ( 'contacts.client_id' , '=' , 'clients.id' )
-> where ( 'contacts.is_primary' , '=' , true )
-> whereNull ( 'contacts.deleted_at' );
})
-> groupBy ( 'clients.id' )
-> havingRaw ( 'count(contacts.id) != 1' );
if ( $this -> option ( 'client_id' )) {
$clients -> where ( 'clients.id' , '=' , $this -> option ( 'client_id' ));
}
$clients = $clients -> get ([ 'clients.id' , DB :: raw ( 'count(contacts.id)' )]);
$this -> logMessage ( count ( $clients ) . ' clients without a single primary contact' );
if ( count ( $clients ) > 0 ) {
$this -> isValid = false ;
}
}
2017-02-21 18:54:00 +01:00
private function checkFailedJobs ()
{
2017-07-31 15:37:20 +02:00
if ( Utils :: isTravis ()) {
return ;
}
2017-08-30 14:21:50 +02:00
$queueDB = config ( 'queue.connections.database.connection' );
2017-08-30 14:24:01 +02:00
$count = DB :: connection ( $queueDB ) -> table ( 'failed_jobs' ) -> count ();
2017-02-21 18:54:00 +01:00
if ( $count > 0 ) {
$this -> isValid = false ;
}
$this -> logMessage ( $count . ' failed jobs' );
}
2016-08-29 17:03:08 +02:00
private function checkBlankInvoiceHistory ()
{
$count = DB :: table ( 'activities' )
-> where ( 'activity_type_id' , '=' , 5 )
-> where ( 'json_backup' , '=' , '' )
2016-11-09 15:08:22 +01:00
-> where ( 'id' , '>' , 858720 )
2016-08-29 17:03:08 +02:00
-> count ();
2016-09-19 10:54:01 +02:00
if ( $count > 0 ) {
$this -> isValid = false ;
}
$this -> logMessage ( $count . ' activities with blank invoice backup' );
2016-08-29 17:03:08 +02:00
}
2017-04-30 15:09:17 +02:00
private function checkInvitations ()
{
$invoices = DB :: table ( 'invoices' )
2017-06-05 10:44:51 +02:00
-> leftJoin ( 'invitations' , function ( $join ) {
$join -> on ( 'invitations.invoice_id' , '=' , 'invoices.id' )
-> whereNull ( 'invitations.deleted_at' );
})
2017-04-30 15:09:17 +02:00
-> groupBy ( 'invoices.id' , 'invoices.user_id' , 'invoices.account_id' , 'invoices.client_id' )
-> havingRaw ( 'count(invitations.id) = 0' )
-> get ([ 'invoices.id' , 'invoices.user_id' , 'invoices.account_id' , 'invoices.client_id' ]);
$this -> logMessage ( count ( $invoices ) . ' invoices without any invitations' );
if ( count ( $invoices ) > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $invoices as $invoice ) {
$invitation = new Invitation ();
$invitation -> account_id = $invoice -> account_id ;
$invitation -> user_id = $invoice -> user_id ;
$invitation -> invoice_id = $invoice -> id ;
$invitation -> contact_id = Contact :: whereClientId ( $invoice -> client_id ) -> whereIsPrimary ( true ) -> first () -> id ;
$invitation -> invitation_key = strtolower ( str_random ( RANDOM_KEY_LENGTH ));
$invitation -> public_id = Invitation :: whereAccountId ( $invoice -> account_id ) -> withTrashed () -> max ( 'public_id' ) + 1 ;
$invitation -> save ();
}
}
}
2015-12-09 16:47:14 +01:00
private function checkAccountData ()
2015-12-02 14:26:06 +01:00
{
2015-12-09 16:47:14 +01:00
$tables = [
'activities' => [
ENTITY_INVOICE ,
ENTITY_CLIENT ,
ENTITY_CONTACT ,
ENTITY_PAYMENT ,
ENTITY_INVITATION ,
2017-01-30 20:40:43 +01:00
ENTITY_USER ,
2015-12-09 16:47:14 +01:00
],
'invoices' => [
ENTITY_CLIENT ,
2017-01-30 20:40:43 +01:00
ENTITY_USER ,
2015-12-09 16:47:14 +01:00
],
'payments' => [
ENTITY_INVOICE ,
ENTITY_CLIENT ,
ENTITY_USER ,
ENTITY_INVITATION ,
2017-01-30 20:40:43 +01:00
ENTITY_CONTACT ,
2015-12-09 16:47:14 +01:00
],
'tasks' => [
ENTITY_INVOICE ,
ENTITY_CLIENT ,
2017-01-30 20:40:43 +01:00
ENTITY_USER ,
2015-12-09 16:47:14 +01:00
],
'credits' => [
ENTITY_CLIENT ,
2017-01-30 20:40:43 +01:00
ENTITY_USER ,
2015-12-09 16:47:14 +01:00
],
2016-08-29 17:03:08 +02:00
'expenses' => [
ENTITY_CLIENT ,
ENTITY_VENDOR ,
ENTITY_INVOICE ,
2017-01-30 20:40:43 +01:00
ENTITY_USER ,
2017-01-01 19:36:02 +01:00
],
'products' => [
ENTITY_USER ,
],
2017-03-09 18:54:03 +01:00
'vendors' => [
ENTITY_USER ,
],
2017-01-01 19:36:02 +01:00
'expense_categories' => [
ENTITY_USER ,
],
2017-03-09 20:32:10 +01:00
'payment_terms' => [
ENTITY_USER ,
],
2017-01-01 19:36:02 +01:00
'projects' => [
ENTITY_USER ,
ENTITY_CLIENT ,
2017-01-30 20:40:43 +01:00
],
2015-12-02 14:26:06 +01:00
];
2015-12-09 16:47:14 +01:00
foreach ( $tables as $table => $entityTypes ) {
foreach ( $entityTypes as $entityType ) {
2017-01-01 19:36:02 +01:00
$tableName = Utils :: pluralizeEntityType ( $entityType );
2017-05-17 13:23:35 +02:00
$field = $entityType ;
2017-04-30 15:16:48 +02:00
if ( $table == 'accounts' ) {
$accountId = 'id' ;
} else {
$accountId = 'account_id' ;
}
2015-12-09 16:47:14 +01:00
$records = DB :: table ( $table )
2017-04-30 15:16:48 +02:00
-> join ( $tableName , " { $tableName } .id " , '=' , " { $table } . { $field } _id " )
-> where ( " { $table } . { $accountId } " , '!=' , DB :: raw ( " { $tableName } .account_id " ))
2017-01-01 19:36:02 +01:00
-> get ([ " { $table } .id " ]);
2015-12-09 16:47:14 +01:00
if ( count ( $records )) {
2016-09-19 10:54:01 +02:00
$this -> isValid = false ;
$this -> logMessage ( count ( $records ) . " { $table } records with incorrect { $entityType } account id " );
2015-12-09 16:47:14 +01:00
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $records as $record ) {
DB :: table ( $table )
-> where ( 'id' , $record -> id )
-> update ([
'account_id' => $record -> account_id ,
'user_id' => $record -> user_id ,
]);
}
}
2015-03-16 22:45:25 +01:00
}
}
}
2015-12-02 14:26:06 +01:00
}
private function checkPaidToDate ()
{
// update client paid_to_date value
$clients = DB :: table ( 'clients' )
2017-10-22 09:12:06 +02:00
-> leftJoin ( 'invoices' , function ( $join ) {
$join -> on ( 'invoices.client_id' , '=' , 'clients.id' )
-> where ( 'invoices.is_deleted' , '=' , 0 );
})
2017-10-21 23:38:31 +02:00
-> leftJoin ( 'payments' , function ( $join ) {
2017-10-22 09:12:06 +02:00
$join -> on ( 'payments.invoice_id' , '=' , 'invoices.id' )
2017-10-21 23:38:31 +02:00
-> where ( 'payments.payment_status_id' , '!=' , 2 )
-> where ( 'payments.payment_status_id' , '!=' , 3 )
-> where ( 'payments.is_deleted' , '=' , 0 );
})
2017-10-22 09:12:06 +02:00
-> where ( 'clients.updated_at' , '>' , '2017-10-01' )
2015-12-02 14:26:06 +01:00
-> groupBy ( 'clients.id' )
2017-10-22 09:12:06 +02:00
-> havingRaw ( 'clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0)) and clients.paid_to_date != 999999999.9999' )
-> get ([ 'clients.id' , 'clients.paid_to_date' , DB :: raw ( 'sum(coalesce(payments.amount - payments.refunded, 0)) as amount' )]);
2016-09-19 10:54:01 +02:00
$this -> logMessage ( count ( $clients ) . ' clients with incorrect paid to date' );
if ( count ( $clients ) > 0 ) {
$this -> isValid = false ;
}
2016-08-25 16:29:55 +02:00
2017-10-22 09:12:06 +02:00
/*
2015-12-02 14:26:06 +01:00
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $clients as $client ) {
DB :: table ( 'clients' )
-> where ( 'id' , $client -> id )
-> update ([ 'paid_to_date' => $client -> amount ]);
}
}
2017-10-22 09:12:06 +02:00
*/
2015-12-02 14:26:06 +01:00
}
2015-03-16 22:45:25 +01:00
2017-10-21 23:38:31 +02:00
private function checkInvoiceBalances ()
{
$invoices = DB :: table ( 'invoices' )
-> leftJoin ( 'payments' , function ( $join ) {
$join -> on ( 'payments.invoice_id' , '=' , 'invoices.id' )
-> where ( 'payments.payment_status_id' , '!=' , 2 )
-> where ( 'payments.payment_status_id' , '!=' , 3 )
-> where ( 'payments.is_deleted' , '=' , 0 );
})
-> where ( 'invoices.updated_at' , '>' , '2017-10-01' )
-> groupBy ( 'invoices.id' )
-> havingRaw ( '(invoices.amount - invoices.balance) != coalesce(sum(payments.amount - payments.refunded), 0)' )
-> get ([ 'invoices.id' , 'invoices.amount' , 'invoices.balance' , DB :: raw ( 'coalesce(sum(payments.amount - payments.refunded), 0)' )]);
$this -> logMessage ( count ( $invoices ) . ' invoices with incorrect balances' );
if ( count ( $invoices ) > 0 ) {
$this -> isValid = false ;
}
}
private function checkClientBalances ()
2015-12-02 14:26:06 +01:00
{
2015-03-16 22:45:25 +01:00
// find all clients where the balance doesn't equal the sum of the outstanding invoices
$clients = DB :: table ( 'clients' )
-> join ( 'invoices' , 'invoices.client_id' , '=' , 'clients.id' )
2016-08-29 17:03:08 +02:00
-> join ( 'accounts' , 'accounts.id' , '=' , 'clients.account_id' )
2016-09-19 10:54:01 +02:00
-> where ( 'accounts.id' , '!=' , 20432 )
2016-08-29 17:03:08 +02:00
-> where ( 'clients.is_deleted' , '=' , 0 )
-> where ( 'invoices.is_deleted' , '=' , 0 )
2016-12-04 22:51:18 +01:00
-> where ( 'invoices.is_public' , '=' , 1 )
2016-05-26 16:56:54 +02:00
-> where ( 'invoices.invoice_type_id' , '=' , INVOICE_TYPE_STANDARD )
2015-03-16 22:45:25 +01:00
-> where ( 'invoices.is_recurring' , '=' , 0 )
-> havingRaw ( 'abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999' );
2016-08-25 16:29:55 +02:00
2016-08-29 17:03:08 +02:00
if ( $this -> option ( 'client_id' )) {
$clients -> where ( 'clients.id' , '=' , $this -> option ( 'client_id' ));
}
2016-09-19 10:54:01 +02:00
2017-05-04 14:53:36 +02:00
$clients = $clients -> groupBy ( 'clients.id' , 'clients.balance' )
2016-08-25 16:29:55 +02:00
-> orderBy ( 'accounts.company_id' , 'DESC' )
-> get ([ 'accounts.company_id' , 'clients.account_id' , 'clients.id' , 'clients.balance' , 'clients.paid_to_date' , DB :: raw ( 'sum(invoices.balance) actual_balance' )]);
2016-09-19 10:54:01 +02:00
$this -> logMessage ( count ( $clients ) . ' clients with incorrect balance/activities' );
if ( count ( $clients ) > 0 ) {
$this -> isValid = false ;
}
2015-03-16 22:45:25 +01:00
foreach ( $clients as $client ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " === Company: { $client -> company_id } Account: { $client -> account_id } Client: { $client -> id } Balance: { $client -> balance } Actual Balance: { $client -> actual_balance } === " );
2016-07-21 14:35:23 +02:00
$foundProblem = false ;
2015-03-16 22:45:25 +01:00
$lastBalance = 0 ;
2015-05-05 11:48:23 +02:00
$lastAdjustment = 0 ;
$lastCreatedAt = null ;
2015-03-16 22:45:25 +01:00
$clientFix = false ;
$activities = DB :: table ( 'activities' )
-> where ( 'client_id' , '=' , $client -> id )
-> orderBy ( 'activities.id' )
2016-07-21 14:35:23 +02:00
-> get ([ 'activities.id' , 'activities.created_at' , 'activities.activity_type_id' , 'activities.adjustment' , 'activities.balance' , 'activities.invoice_id' ]);
2016-09-19 10:54:01 +02:00
//$this->logMessage(var_dump($activities));
2015-03-16 22:45:25 +01:00
foreach ( $activities as $activity ) {
$activityFix = false ;
if ( $activity -> invoice_id ) {
$invoice = DB :: table ( 'invoices' )
-> where ( 'id' , '=' , $activity -> invoice_id )
2016-05-26 16:56:54 +02:00
-> first ([ 'invoices.amount' , 'invoices.is_recurring' , 'invoices.invoice_type_id' , 'invoices.deleted_at' , 'invoices.id' , 'invoices.is_deleted' ]);
2015-03-16 22:45:25 +01:00
// Check if this invoice was once set as recurring invoice
2017-01-30 20:40:43 +01:00
if ( $invoice && ! $invoice -> is_recurring && DB :: table ( 'invoices' )
2015-03-16 22:45:25 +01:00
-> where ( 'recurring_invoice_id' , '=' , $activity -> invoice_id )
-> first ([ 'invoices.id' ])) {
$invoice -> is_recurring = 1 ;
// **Fix for enabling a recurring invoice to be set as non-recurring**
if ( $this -> option ( 'fix' ) == 'true' ) {
DB :: table ( 'invoices' )
-> where ( 'id' , $invoice -> id )
-> update ([ 'is_recurring' => 1 ]);
}
}
}
if ( $activity -> activity_type_id == ACTIVITY_TYPE_CREATE_INVOICE
|| $activity -> activity_type_id == ACTIVITY_TYPE_CREATE_QUOTE ) {
2016-08-25 16:29:55 +02:00
2015-03-16 22:45:25 +01:00
// Get original invoice amount
$update = DB :: table ( 'activities' )
-> where ( 'invoice_id' , '=' , $activity -> invoice_id )
-> where ( 'activity_type_id' , '=' , ACTIVITY_TYPE_UPDATE_INVOICE )
-> orderBy ( 'id' )
-> first ([ 'json_backup' ]);
if ( $update ) {
$backup = json_decode ( $update -> json_backup );
$invoice -> amount = floatval ( $backup -> amount );
}
$noAdjustment = $activity -> activity_type_id == ACTIVITY_TYPE_CREATE_INVOICE
&& $activity -> adjustment == 0
&& $invoice -> amount > 0 ;
2016-08-29 17:03:08 +02:00
// **Fix for ninja invoices which didn't have the invoice_type_id value set
if ( $noAdjustment && $client -> account_id == 20432 ) {
2017-01-30 20:40:43 +01:00
$this -> logMessage ( 'No adjustment for ninja invoice' );
2016-08-29 17:03:08 +02:00
$foundProblem = true ;
$clientFix += $invoice -> amount ;
$activityFix = $invoice -> amount ;
2015-03-16 22:45:25 +01:00
// **Fix for allowing converting a recurring invoice to a normal one without updating the balance**
2017-01-30 20:40:43 +01:00
} elseif ( $noAdjustment && $invoice -> invoice_type_id == INVOICE_TYPE_STANDARD && ! $invoice -> is_recurring ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " No adjustment for new invoice: { $activity -> invoice_id } amount: { $invoice -> amount } invoiceTypeId: { $invoice -> invoice_type_id } isRecurring: { $invoice -> is_recurring } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$clientFix += $invoice -> amount ;
$activityFix = $invoice -> amount ;
// **Fix for updating balance when creating a quote or recurring invoice**
2016-05-26 16:56:54 +02:00
} elseif ( $activity -> adjustment != 0 && ( $invoice -> invoice_type_id == INVOICE_TYPE_QUOTE || $invoice -> is_recurring )) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for new invoice: { $activity -> invoice_id } adjustment: { $activity -> adjustment } invoiceTypeId: { $invoice -> invoice_type_id } isRecurring: { $invoice -> is_recurring } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$clientFix -= $activity -> adjustment ;
$activityFix = 0 ;
}
} elseif ( $activity -> activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE ) {
// **Fix for updating balance when deleting a recurring invoice**
if ( $activity -> adjustment != 0 && $invoice -> is_recurring ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for deleted invoice adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
if ( $activity -> balance != $lastBalance ) {
$clientFix -= $activity -> adjustment ;
}
$activityFix = 0 ;
}
} elseif ( $activity -> activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE ) {
// **Fix for updating balance when archiving an invoice**
2017-01-30 20:40:43 +01:00
if ( $activity -> adjustment != 0 && ! $invoice -> is_recurring ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for archiving invoice adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$activityFix = 0 ;
$clientFix += $activity -> adjustment ;
}
} elseif ( $activity -> activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE ) {
// **Fix for updating balance when updating recurring invoice**
if ( $activity -> adjustment != 0 && $invoice -> is_recurring ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for updated recurring invoice adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$clientFix -= $activity -> adjustment ;
$activityFix = 0 ;
2017-01-30 17:05:31 +01:00
} elseif (( strtotime ( $activity -> created_at ) - strtotime ( $lastCreatedAt ) <= 1 ) && $activity -> adjustment > 0 && $activity -> adjustment == $lastAdjustment ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Duplicate adjustment for updated invoice adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-05-05 11:48:23 +02:00
$clientFix -= $activity -> adjustment ;
$activityFix = 0 ;
2015-03-16 22:45:25 +01:00
}
} elseif ( $activity -> activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE ) {
// **Fix for updating balance when updating a quote**
if ( $activity -> balance != $lastBalance ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for updated quote adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$clientFix += $lastBalance - $activity -> balance ;
$activityFix = 0 ;
}
2017-01-30 17:05:31 +01:00
} elseif ( $activity -> activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT ) {
2015-09-20 23:05:02 +02:00
// **Fix for deleting payment after deleting invoice**
2015-03-16 22:45:25 +01:00
if ( $activity -> adjustment != 0 && $invoice -> is_deleted && $activity -> created_at > $invoice -> deleted_at ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Incorrect adjustment for deleted payment adjustment: { $activity -> adjustment } " );
2016-07-21 14:35:23 +02:00
$foundProblem = true ;
2015-03-16 22:45:25 +01:00
$activityFix = 0 ;
$clientFix -= $activity -> adjustment ;
}
}
if ( $activityFix !== false || $clientFix !== false ) {
$data = [
2017-01-30 20:40:43 +01:00
'balance' => $activity -> balance + $clientFix ,
2015-03-16 22:45:25 +01:00
];
if ( $activityFix !== false ) {
$data [ 'adjustment' ] = $activityFix ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
DB :: table ( 'activities' )
-> where ( 'id' , $activity -> id )
-> update ( $data );
}
}
$lastBalance = $activity -> balance ;
2015-05-05 11:48:23 +02:00
$lastAdjustment = $activity -> adjustment ;
$lastCreatedAt = $activity -> created_at ;
2015-03-16 22:45:25 +01:00
}
2015-05-05 11:48:23 +02:00
if ( $activity -> balance + $clientFix != $client -> actual_balance ) {
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " ** Creating 'recovered update' activity ** " );
2015-03-16 22:45:25 +01:00
if ( $this -> option ( 'fix' ) == 'true' ) {
2015-05-05 11:48:23 +02:00
DB :: table ( 'activities' ) -> insert ([
2017-01-30 20:40:43 +01:00
'created_at' => new Carbon (),
'updated_at' => new Carbon (),
2015-05-05 11:48:23 +02:00
'account_id' => $client -> account_id ,
'client_id' => $client -> id ,
'adjustment' => $client -> actual_balance - $activity -> balance ,
'balance' => $client -> actual_balance ,
]);
2015-03-16 22:45:25 +01:00
}
}
2015-05-05 11:48:23 +02:00
$data = [ 'balance' => $client -> actual_balance ];
2016-09-19 10:54:01 +02:00
$this -> logMessage ( " Corrected balance: { $client -> actual_balance } " );
2015-05-05 11:48:23 +02:00
if ( $this -> option ( 'fix' ) == 'true' ) {
DB :: table ( 'clients' )
-> where ( 'id' , $client -> id )
-> update ( $data );
}
2015-03-16 22:45:25 +01:00
}
}
2016-07-03 18:11:58 +02:00
/**
* @ return array
*/
2015-03-16 22:45:25 +01:00
protected function getArguments ()
{
2016-07-03 18:11:58 +02:00
return [];
2015-03-16 22:45:25 +01:00
}
2016-07-03 18:11:58 +02:00
/**
* @ return array
*/
2015-03-16 22:45:25 +01:00
protected function getOptions ()
{
2016-07-03 18:11:58 +02:00
return [
[ 'fix' , null , InputOption :: VALUE_OPTIONAL , 'Fix data' , null ],
2017-11-07 09:40:46 +01:00
[ 'fast' , null , InputOption :: VALUE_OPTIONAL , 'Fast' , null ],
2016-07-03 18:11:58 +02:00
[ 'client_id' , null , InputOption :: VALUE_OPTIONAL , 'Client id' , null ],
2017-05-01 14:17:52 +02:00
[ 'database' , null , InputOption :: VALUE_OPTIONAL , 'Database' , null ],
2016-07-03 18:11:58 +02:00
];
2015-03-16 22:45:25 +01:00
}
2016-08-25 16:29:55 +02:00
}