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
*
2022-04-27 05:20:41 +02:00
* @ copyright Copyright ( c ) 2022. Invoice Ninja LLC ( https :// invoiceninja . com )
2020-09-07 12:18:56 +02:00
*
2021-06-16 08:58:16 +02:00
* @ license https :// www . elastic . co / licensing / elastic - license
2020-09-07 12:18:56 +02:00
*/
2020-04-19 12:29:58 +02:00
namespace App\Console\Commands ;
use App ;
2022-07-27 09:37:37 +02:00
use App\DataMapper\ClientSettings ;
2021-06-05 07:58:37 +02:00
use App\Factory\ClientContactFactory ;
2022-02-22 12:58:04 +01:00
use App\Factory\VendorContactFactory ;
2020-04-19 12:29:58 +02:00
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 ;
2021-07-01 12:58:38 +02:00
use App\Models\Company ;
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 ;
2021-11-03 10:37:17 +01:00
use App\Models\CreditInvitation ;
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 ;
2021-07-01 12:58:38 +02:00
use App\Models\Paymentable ;
2021-09-08 05:45:10 +02:00
use App\Models\QuoteInvitation ;
use App\Models\RecurringInvoiceInvitation ;
2022-08-29 09:07:20 +02:00
use App\Models\User ;
2022-02-25 12:36:52 +01:00
use App\Models\Vendor ;
2020-04-19 12:29:58 +02:00
use App\Utils\Ninja ;
use Exception ;
use Illuminate\Console\Command ;
2021-09-08 01:29:20 +02:00
use Illuminate\Support\Facades\DB ;
use Illuminate\Support\Facades\Mail ;
2021-07-01 12:58:38 +02:00
use Illuminate\Support\Str ;
2020-04-19 12:29:58 +02:00
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
*/
2022-05-25 08:34:43 +02:00
protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=}' ;
2020-04-19 12:29:58 +02:00
/**
* @ var string
*/
protected $description = 'Check/fix data' ;
protected $log = '' ;
2021-02-10 02:59:30 +01:00
2020-04-19 12:29:58 +02:00
protected $isValid = true ;
2021-06-21 00:18:30 +02:00
protected $wrong_paid_to_dates = 0 ;
protected $wrong_balances = 0 ;
2022-05-25 08:34:43 +02:00
protected $wrong_paid_status = 0 ;
2020-04-19 12:29:58 +02:00
public function handle ()
{
2021-06-27 06:52:15 +02:00
$time_start = microtime ( true );
2021-06-05 07:58:37 +02:00
$database_connection = $this -> option ( 'database' ) ? $this -> option ( 'database' ) : 'Connected to Default DB' ;
$fix_status = $this -> option ( 'fix' ) ? " Fixing Issues " : " Just checking issues " ;
$this -> logMessage ( date ( 'Y-m-d h:i:s' ) . ' Running CheckData... on ' . $database_connection . " Fix Status = { $fix_status } " );
2020-04-19 12:29:58 +02:00
if ( $database = $this -> option ( 'database' )) {
config ([ 'database.default' => $database ]);
}
2022-07-17 07:26:35 +02:00
$this -> checkInvoiceBalances ();
$this -> checkClientBalanceEdgeCases ();
2021-11-03 10:37:17 +01:00
$this -> checkPaidToDatesNew ();
2020-04-19 12:29:58 +02:00
$this -> checkContacts ();
2022-02-22 12:58:04 +01:00
$this -> checkVendorContacts ();
2021-09-08 01:29:20 +02:00
$this -> checkEntityInvitations ();
2020-06-28 00:24:08 +02:00
$this -> checkCompanyData ();
2022-05-25 08:34:43 +02:00
$this -> checkBalanceVsPaidStatus ();
2022-08-28 03:08:15 +02:00
$this -> checkDuplicateRecurringInvoices ();
2022-08-29 09:07:20 +02:00
$this -> checkOauthSanity ();
2022-02-18 21:12:00 +01:00
if ( Ninja :: isHosted ())
$this -> checkAccountStatuses ();
2020-04-19 12:29:58 +02:00
if ( ! $this -> option ( 'client_id' )) {
$this -> checkOAuth ();
}
2020-09-06 11:38:10 +02:00
$this -> logMessage ( 'Done: ' . strtoupper ( $this -> isValid ? Account :: RESULT_SUCCESS : Account :: RESULT_FAILURE ));
2021-06-27 06:52:15 +02:00
$this -> logMessage ( 'Total execution time in seconds: ' . ( microtime ( true ) - $time_start ));
2020-04-19 12:29:58 +02:00
$errorEmail = config ( 'ninja.error_email' );
2022-07-27 09:37:37 +02:00
if ( strlen ( $errorEmail ) > 1 ) {
2020-04-19 12:29:58 +02:00
Mail :: raw ( $this -> log , function ( $message ) use ( $errorEmail , $database ) {
$message -> to ( $errorEmail )
2021-03-04 09:42:22 +01:00
-> from ( config ( 'mail.from.address' ), config ( 'mail.from.name' ))
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
}
2022-08-29 09:07:20 +02:00
private function checkOauthSanity ()
{
User :: where ( 'oauth_provider_id' , '1' ) -> cursor () -> each ( function ( $user ){
$this -> logMessage ( " Invalid provider ID for user id# { $user -> id } " );
});
}
2022-08-28 03:08:15 +02:00
private function checkDuplicateRecurringInvoices ()
{
if ( Ninja :: isHosted ())
{
$c = Client :: on ( 'db-ninja-01' ) -> where ( 'company_id' , config ( 'ninja.ninja_default_company_id' ))
-> with ( 'recurring_invoices' )
-> cursor ()
-> each ( function ( $client ){
if ( $client -> recurring_invoices () -> where ( 'is_deleted' , 0 ) -> where ( 'deleted_at' , null ) -> count () > 1 )
$this -> logMessage ( " Duplicate Recurring Invoice => { $client -> custom_value1 } " );
});
}
}
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 ([
2021-09-08 05:45:10 +02:00
'contact_key' => Str :: random ( config ( 'ninja.key_length' )),
2020-04-19 12:29:58 +02:00
]);
}
}
// 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 ) {
2021-06-05 07:58:37 +02:00
$this -> logMessage ( " Fixing missing contacts # { $client -> id } " );
$new_contact = ClientContactFactory :: create ( $client -> company_id , $client -> user_id );
$new_contact -> client_id = $client -> id ;
$new_contact -> contact_key = Str :: random ( 40 );
$new_contact -> is_primary = true ;
$new_contact -> save ();
2020-04-19 12:29:58 +02:00
}
}
}
2022-02-22 12:58:04 +01:00
private function checkVendorContacts ()
{
// check for contacts with the contact_key value set
$contacts = DB :: table ( 'vendor_contacts' )
-> whereNull ( 'contact_key' )
-> orderBy ( 'id' )
-> get ([ 'id' ]);
$this -> logMessage ( $contacts -> count () . ' contacts without a contact_key' );
if ( $contacts -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
foreach ( $contacts as $contact ) {
DB :: table ( 'vendor_contacts' )
-> where ( 'id' , '=' , $contact -> id )
-> whereNull ( 'contact_key' )
-> update ([
'contact_key' => Str :: random ( config ( 'ninja.key_length' )),
]);
}
}
// check for missing contacts
$vendors = DB :: table ( 'vendors' )
-> leftJoin ( 'vendor_contacts' , function ( $join ) {
$join -> on ( 'vendor_contacts.vendor_id' , '=' , 'vendors.id' )
-> whereNull ( 'vendor_contacts.deleted_at' );
})
-> groupBy ( 'vendors.id' , 'vendors.user_id' , 'vendors.company_id' )
-> havingRaw ( 'count(vendor_contacts.id) = 0' );
if ( $this -> option ( 'vendor_id' )) {
$vendors -> where ( 'vendors.id' , '=' , $this -> option ( 'vendor_id' ));
}
$vendors = $vendors -> get ([ 'vendors.id' , 'vendors.user_id' , 'vendors.company_id' ]);
$this -> logMessage ( $vendors -> count () . ' vendors without any contacts' );
if ( $vendors -> count () > 0 ) {
$this -> isValid = false ;
}
if ( $this -> option ( 'fix' ) == 'true' ) {
2022-02-25 12:36:52 +01:00
$vendors = Vendor :: withTrashed () -> doesntHave ( 'contacts' ) -> get ();
2022-02-22 12:58:04 +01:00
foreach ( $vendors as $vendor ) {
$this -> logMessage ( " Fixing missing vendor contacts # { $vendor -> id } " );
$new_contact = VendorContactFactory :: create ( $vendor -> company_id , $vendor -> user_id );
$new_contact -> vendor_id = $vendor -> id ;
$new_contact -> contact_key = Str :: random ( 40 );
$new_contact -> is_primary = true ;
$new_contact -> save ();
}
}
}
2020-04-19 12:29:58 +02:00
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 ;
2021-09-08 01:29:20 +02:00
$invitation -> contact_id = ClientContact :: whereClientId ( $invoice -> client_id ) -> first () -> id ;
$invitation -> invitation_key = Str :: random ( config ( 'ninja.key_length' ));
2020-04-19 12:29:58 +02:00
$invitation -> save ();
}
}
}
2021-09-08 01:29:20 +02:00
private function checkEntityInvitations ()
{
2021-09-08 05:45:10 +02:00
RecurringInvoiceInvitation :: where ( 'deleted_at' , " 0000-00-00 00:00:00.000000 " ) -> withTrashed () -> update ([ 'deleted_at' => null ]);
InvoiceInvitation :: where ( 'deleted_at' , " 0000-00-00 00:00:00.000000 " ) -> withTrashed () -> update ([ 'deleted_at' => null ]);
QuoteInvitation :: where ( 'deleted_at' , " 0000-00-00 00:00:00.000000 " ) -> withTrashed () -> update ([ 'deleted_at' => null ]);
2021-11-03 10:37:17 +01:00
CreditInvitation :: where ( 'deleted_at' , " 0000-00-00 00:00:00.000000 " ) -> withTrashed () -> update ([ 'deleted_at' => null ]);
2021-09-08 01:29:20 +02:00
$entities = [ 'invoice' , 'quote' , 'credit' , 'recurring_invoice' ];
foreach ( $entities as $entity )
{
$table = " { $entity } s " ;
$invitation_table = " { $entity } _invitations " ;
$entities = DB :: table ( $table )
-> leftJoin ( $invitation_table , function ( $join ) use ( $invitation_table , $table , $entity ){
2021-09-08 05:55:45 +02:00
$join -> on ( " { $invitation_table } . { $entity } _id " , '=' , " { $table } .id " );
// ->whereNull("{$invitation_table}.deleted_at");
2021-09-08 01:29:20 +02:00
})
-> groupBy ( " { $table } .id " , " { $table } .user_id " , " { $table } .company_id " , " { $table } .client_id " )
-> havingRaw ( " count( { $invitation_table } .id) = 0 " )
-> get ([ " { $table } .id " , " { $table } .user_id " , " { $table } .company_id " , " { $table } .client_id " ]);
$this -> logMessage ( $entities -> count () . " { $table } without any invitations " );
if ( $this -> option ( 'fix' ) == 'true' )
$this -> fixInvitations ( $entities , $entity );
}
}
private function fixInvitations ( $entities , $entity )
{
$entity_key = " { $entity } _id " ;
$entity_obj = 'App\Models\\' . ucfirst ( Str :: camel ( $entity )) . 'Invitation' ;
foreach ( $entities as $entity )
{
$invitation = new $entity_obj ();
$invitation -> company_id = $entity -> company_id ;
$invitation -> user_id = $entity -> user_id ;
$invitation -> { $entity_key } = $entity -> id ;
$invitation -> client_contact_id = ClientContact :: whereClientId ( $entity -> client_id ) -> first () -> id ;
$invitation -> key = Str :: random ( config ( 'ninja.key_length' ));
2021-09-08 05:45:10 +02:00
try {
$invitation -> save ();
}
catch ( \Exception $e ){
$invitation = null ;
}
2021-09-08 01:29:20 +02:00
}
}
2021-11-01 12:15:56 +01:00
private function clientPaidToDateQuery ()
{
$results = \DB :: select ( \DB :: raw ( "
SELECT
clients . id as client_id ,
clients . paid_to_date as client_paid_to_date ,
SUM ( coalesce ( payments . amount - payments . refunded , 0 )) as payments_applied
FROM clients
INNER JOIN
payments ON
clients . id = payments . client_id
WHERE payments . status_id IN ( 1 , 4 , 5 , 6 )
AND clients . is_deleted = false
AND payments . is_deleted = false
GROUP BY clients . id
HAVING payments_applied != client_paid_to_date
ORDER BY clients . id ;
" ) );
return $results ;
}
private function clientCreditPaymentables ( $client )
{
$results = \DB :: select ( \DB :: raw ( "
SELECT
SUM ( paymentables . amount - paymentables . refunded ) as credit_payment
FROM payments
LEFT JOIN paymentables
ON
payments . id = paymentables . payment_id
2022-03-01 11:25:18 +01:00
WHERE paymentable_type = ?
2021-11-01 12:15:56 +01:00
AND paymentables . deleted_at is NULL
2022-03-16 03:06:25 +01:00
AND paymentables . amount > 0
2022-02-19 06:11:30 +01:00
AND payments . is_deleted = 0
2021-11-07 07:12:51 +01:00
AND payments . client_id = ? ;
2022-03-01 11:25:18 +01:00
" ), [App \ Models \ Credit::class, $client->id ] );
2021-11-01 12:15:56 +01:00
return $results ;
}
private function checkPaidToDatesNew ()
{
$clients_to_check = $this -> clientPaidToDateQuery ();
$this -> wrong_paid_to_dates = 0 ;
foreach ( $clients_to_check as $_client )
{
2021-11-03 10:37:17 +01:00
$client = Client :: withTrashed () -> find ( $_client -> client_id );
2021-11-01 12:15:56 +01:00
2022-03-10 02:17:05 +01:00
$credits_from_reversal = Credit :: withTrashed () -> where ( 'client_id' , $client -> id ) -> where ( 'is_deleted' , 0 ) -> whereNotNull ( 'invoice_id' ) -> sum ( 'amount' );
2021-11-01 12:15:56 +01:00
$credits_used_for_payments = $this -> clientCreditPaymentables ( $client );
2022-03-10 02:17:05 +01:00
$total_paid_to_date = $_client -> payments_applied + $credits_used_for_payments [ 0 ] -> credit_payment - $credits_from_reversal ;
2021-11-01 12:15:56 +01:00
2021-11-03 10:37:17 +01:00
if ( round ( $total_paid_to_date , 2 ) != round ( $_client -> client_paid_to_date , 2 )){
2021-11-01 12:15:56 +01:00
$this -> wrong_paid_to_dates ++ ;
2022-03-01 11:25:18 +01:00
$this -> logMessage ( $client -> present () -> name . ' id = # ' . $client -> id . " - Client Paid To Date = { $client -> paid_to_date } != Invoice Payments = { $total_paid_to_date } - { $_client -> payments_applied } + { $credits_used_for_payments [ 0 ] -> credit_payment } " );
2021-11-01 12:15:56 +01:00
$this -> isValid = false ;
if ( $this -> option ( 'paid_to_date' )){
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " Fixing { $client -> paid_to_date } to { $total_paid_to_date } " );
$client -> paid_to_date = $total_paid_to_date ;
$client -> save ();
}
}
}
2021-11-05 05:24:37 +01:00
$this -> logMessage ( " { $this -> wrong_paid_to_dates } clients with incorrect paid to dates " );
2021-11-01 12:15:56 +01:00
}
2020-07-02 07:25:34 +02:00
private function checkPaidToDates ()
{
2021-06-21 00:18:30 +02:00
$this -> 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
2021-07-02 07:42:51 +02:00
$clients = DB :: table ( 'clients' )
-> leftJoin ( 'payments' , function ( $join ) {
$join -> on ( 'payments.client_id' , '=' , 'clients.id' )
-> where ( 'payments.is_deleted' , 0 )
-> whereIn ( 'payments.status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ]);
})
-> where ( 'clients.is_deleted' , 0 )
-> where ( 'clients.updated_at' , '>' , now () -> subDays ( 2 ))
-> groupBy ( 'clients.id' )
-> havingRaw ( 'clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0))' )
-> get ([ 'clients.id' , 'clients.paid_to_date' , DB :: raw ( 'sum(coalesce(payments.amount - payments.refunded, 0)) as amount' )]);
/* Due to accounting differences we need to perform a second loop here to ensure there actually is an issue */
$clients -> each ( function ( $client_record ) use ( $credit_total_applied ) {
2021-07-07 00:11:44 +02:00
$client = Client :: withTrashed () -> find ( $client_record -> id );
2020-11-25 10:21:26 +01:00
2021-07-02 07:42:51 +02:00
$total_invoice_payments = 0 ;
2021-06-30 10:28:30 +02:00
2021-07-02 07:42:51 +02:00
foreach ( $client -> invoices () -> where ( 'is_deleted' , false ) -> where ( 'status_id' , '>' , 1 ) -> get () as $invoice ) {
2021-06-30 10:28:30 +02:00
$total_invoice_payments += $invoice -> payments ()
-> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ])
-> selectRaw ( 'sum(paymentables.amount - paymentables.refunded) as p' )
-> pluck ( 'p' )
-> first ();
2020-09-06 11:38:10 +02:00
2021-06-27 09:48:35 +02:00
}
2021-06-27 06:52:15 +02:00
2021-06-27 09:48:35 +02:00
//commented IN 27/06/2021 - sums ALL client payments AND the unapplied amounts to match the client paid to date
2021-06-27 06:52:15 +02:00
$p = Payment :: where ( 'client_id' , $client -> id )
-> where ( 'is_deleted' , 0 )
2021-07-01 12:58:38 +02:00
-> whereIn ( 'status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ])
-> sum ( DB :: Raw ( 'amount - applied' ));
2021-06-27 06:52:15 +02:00
2021-07-01 12:58:38 +02:00
$total_invoice_payments += $p ;
2020-09-06 11:38:10 +02:00
2021-02-11 12:37:58 +01:00
// 10/02/21
foreach ( $client -> payments as $payment ) {
2021-07-02 07:42:51 +02:00
$credit_total_applied += $payment -> paymentables ()
-> where ( 'paymentable_type' , App\Models\Credit :: class )
-> selectRaw ( 'sum(paymentables.amount - paymentables.refunded) as p' )
-> pluck ( 'p' )
-> first ();
2021-02-11 12:37:58 +01:00
}
if ( $credit_total_applied < 0 ) {
$total_invoice_payments += $credit_total_applied ;
}
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 )) {
2021-06-21 00:18:30 +02:00
$this -> wrong_paid_to_dates ++ ;
2020-07-02 07:25:34 +02:00
2021-06-05 07:58:37 +02: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 ;
2021-06-22 11:02:26 +02:00
if ( $this -> option ( 'paid_to_date' )){
2021-06-22 13:14:08 +02:00
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " Fixing { $client -> paid_to_date } to { $total_invoice_payments } " );
2021-06-22 11:02:26 +02:00
$client -> paid_to_date = $total_invoice_payments ;
$client -> save ();
}
2020-07-02 07:25:34 +02:00
}
});
2021-06-21 00:18:30 +02:00
$this -> logMessage ( " { $this -> wrong_paid_to_dates } clients with incorrect paid to dates " );
2020-07-02 07:25:34 +02:00
}
2020-07-02 06:23:30 +02:00
private function checkInvoicePayments ()
{
2021-06-21 00:18:30 +02:00
$this -> wrong_balances = 0 ;
2020-07-02 06:23:30 +02:00
2021-07-02 07:42:51 +02:00
Client :: cursor () -> where ( 'is_deleted' , 0 ) -> where ( 'clients.updated_at' , '>' , now () -> subDays ( 2 )) -> each ( function ( $client ) {
2021-06-09 12:19:23 +02:00
2021-06-21 00:18:30 +02:00
$client -> invoices -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , '!=' , Invoice :: STATUS_DRAFT ) -> each ( function ( $invoice ) use ( $client ) {
2021-06-30 10:32:31 +02:00
$total_paid = $invoice -> payments ()
-> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ Payment :: STATUS_COMPLETED , Payment :: STATUS_PENDING , Payment :: STATUS_PARTIALLY_REFUNDED , Payment :: STATUS_REFUNDED ])
-> selectRaw ( 'sum(paymentables.amount - paymentables.refunded) as p' )
-> pluck ( 'p' )
-> first ();
2020-07-16 05:54:26 +02:00
2021-06-30 10:32:31 +02:00
$total_credit = $invoice -> credits () -> get () -> sum ( 'amount' );
2021-06-30 10:28:30 +02:00
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 )) {
2021-06-21 00:18:30 +02:00
$this -> wrong_balances ++ ;
2020-07-02 06:23:30 +02:00
2021-06-30 10:32:31 +02:00
$this -> logMessage ( $client -> present () -> name . ' - ' . $client -> id . " - Total Paid = { $total_paid } != Calculated Total = { $calculated_paid_amount } " );
2020-07-02 06:23:30 +02:00
$this -> isValid = false ;
}
});
2021-06-09 12:19:23 +02:00
2020-07-02 06:23:30 +02:00
});
2020-05-26 14:37:15 +02:00
2021-06-21 00:18:30 +02:00
$this -> logMessage ( " { $this -> wrong_balances } clients with incorrect invoice balances " );
2020-04-19 12:29:58 +02:00
}
2021-10-31 07:01:51 +01:00
private function clientBalanceQuery ()
{
$results = \DB :: select ( \DB :: raw ( "
2021-11-04 08:25:15 +01:00
SELECT
SUM ( invoices . balance ) as invoice_balance ,
clients . id as client_id ,
clients . balance as client_balance
FROM clients
LEFT JOIN
invoices ON
clients . id = invoices . client_id
WHERE invoices . is_deleted = false
2022-05-15 09:51:06 +02:00
AND invoices . status_id IN ( 2 , 3 )
2021-11-04 08:25:15 +01:00
GROUP BY clients . id
HAVING invoice_balance != clients . balance
ORDER BY clients . id ;
2021-10-31 07:01:51 +01:00
" ) );
return $results ;
}
2021-07-02 12:03:44 +02:00
2020-04-19 12:29:58 +02:00
private function checkClientBalances ()
{
2021-06-21 00:18:30 +02:00
$this -> wrong_balances = 0 ;
$this -> wrong_paid_to_dates = 0 ;
2020-05-26 14:37:15 +02:00
2021-10-31 07:01:51 +01:00
$clients = $this -> clientBalanceQuery ();
2020-11-24 11:12:05 +01:00
2021-10-31 07:01:51 +01:00
foreach ( $clients as $client )
{
2021-11-01 12:15:56 +01:00
$client = ( array ) $client ;
2022-05-13 06:06:21 +02:00
if (( string ) $client [ 'invoice_balance' ] != ( string ) $client [ 'client_balance' ]) {
2021-06-21 00:18:30 +02:00
$this -> wrong_paid_to_dates ++ ;
2020-05-26 14:37:15 +02:00
2021-11-04 06:00:49 +01:00
$client_object = Client :: withTrashed () -> find ( $client [ 'client_id' ]);
2021-10-31 07:01:51 +01:00
2022-05-13 06:06:21 +02:00
$this -> logMessage ( $client_object -> present () -> name . ' - ' . $client_object -> id . " - calculated client balances do not match Invoice Balances = " . $client [ 'invoice_balance' ] . " - Client Balance = " . rtrim ( $client [ 'client_balance' ], '0' ));
2021-10-31 07:01:51 +01:00
2022-05-13 06:06:21 +02:00
if ( $this -> option ( 'client_balance' )){
2021-10-31 07:01:51 +01:00
2022-05-13 06:06:21 +02:00
$this -> logMessage ( " # { $client_object -> id } " . $client_object -> present () -> name . ' - ' . $client_object -> number . " Fixing { $client_object -> balance } to " . $client [ 'invoice_balance' ]);
$client_object -> balance = $client [ 'invoice_balance' ];
2021-11-04 08:25:15 +01:00
$client_object -> save ();
2021-10-31 07:01:51 +01:00
}
2021-07-13 11:09:02 +02:00
2021-10-31 07:01:51 +01:00
$this -> isValid = false ;
2020-05-26 14:37:15 +02:00
}
2021-10-31 07:01:51 +01:00
2020-09-06 11:38:10 +02:00
}
2020-05-26 14:37:15 +02:00
2021-06-21 00:18:30 +02:00
$this -> logMessage ( " { $this -> wrong_paid_to_dates } clients with incorrect client balances " );
2020-04-19 12:29:58 +02:00
}
2022-07-17 07:26:35 +02:00
private function checkClientBalanceEdgeCases ()
{
Client :: query ()
-> where ( 'is_deleted' , false )
-> where ( 'balance' , '!=' , 0 )
-> cursor ()
-> each ( function ( $client ){
$count = Invoice :: withTrashed ()
-> where ( 'client_id' , $client -> id )
-> where ( 'is_deleted' , false )
-> whereIn ( 'status_id' , [ 2 , 3 ])
-> count ();
if ( $count == 0 ){
$this -> logMessage ( " # { $client -> id } # { $client -> name } { $client -> balance } is invalid should be 0 " );
if ( $this -> option ( 'client_balance' )){
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " Fixing { $client -> balance } to 0 " );
$client -> balance = 0 ;
$client -> save ();
}
}
});
}
2021-11-03 10:37:17 +01:00
private function invoiceBalanceQuery ()
{
$results = \DB :: select ( \DB :: raw ( "
SELECT
clients . id ,
clients . balance ,
SUM ( invoices . balance ) as invoices_balance
FROM clients
JOIN invoices
ON invoices . client_id = clients . id
WHERE invoices . is_deleted = 0
AND clients . is_deleted = 0
2022-05-15 09:51:06 +02:00
AND invoices . status_id IN ( 2 , 3 )
2021-11-03 10:37:17 +01:00
GROUP BY clients . id
HAVING ( invoices_balance != clients . balance )
ORDER BY clients . id ;
" ) );
return $results ;
}
2022-05-13 06:06:21 +02:00
private function checkInvoiceBalances ()
2021-11-03 10:37:17 +01:00
{
$this -> wrong_balances = 0 ;
$this -> wrong_paid_to_dates = 0 ;
$_clients = $this -> invoiceBalanceQuery ();
foreach ( $_clients as $_client )
{
$client = Client :: withTrashed () -> find ( $_client -> id );
2022-05-15 09:51:06 +02:00
$invoice_balance = $client -> invoices () -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ 2 , 3 ]) -> sum ( 'balance' );
2021-11-03 10:37:17 +01:00
$ledger = CompanyLedger :: where ( 'client_id' , $client -> id ) -> orderBy ( 'id' , 'DESC' ) -> first ();
2022-05-13 06:06:21 +02:00
if ( number_format ( $invoice_balance , 4 ) != number_format ( $client -> balance , 4 )) {
2021-11-03 10:37:17 +01:00
$this -> wrong_balances ++ ;
2022-05-13 06:06:21 +02:00
$ledger_balance = $ledger ? $ledger -> balance : 0 ;
2021-11-03 10:37:17 +01:00
2022-05-13 06:06:21 +02:00
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " - Balance Failure - Invoice Balances = { $invoice_balance } Client Balance = { $client -> balance } Ledger Balance = { $ledger_balance } " );
2021-11-03 10:37:17 +01:00
2022-05-13 06:06:21 +02:00
$this -> isValid = false ;
2021-11-03 10:37:17 +01:00
if ( $this -> option ( 'client_balance' )){
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " Fixing { $client -> balance } to { $invoice_balance } " );
$client -> balance = $invoice_balance ;
$client -> save ();
2022-05-13 06:06:21 +02:00
}
if ( $ledger && ( number_format ( $invoice_balance , 4 ) != number_format ( $ledger -> balance , 4 )))
{
2021-11-03 10:37:17 +01:00
$ledger -> adjustment = $invoice_balance ;
$ledger -> balance = $invoice_balance ;
$ledger -> notes = 'Ledger Adjustment' ;
$ledger -> save ();
}
2022-05-13 06:06:21 +02:00
2021-11-03 10:37:17 +01:00
}
}
$this -> logMessage ( " { $this -> wrong_balances } clients with incorrect balances " );
}
2022-05-13 06:06:21 +02:00
private function checkLedgerBalances ()
2021-02-03 13:29:44 +01:00
{
2021-06-21 00:18:30 +02:00
$this -> wrong_balances = 0 ;
$this -> wrong_paid_to_dates = 0 ;
2021-02-03 13:29:44 +01:00
2021-07-02 07:42:51 +02:00
foreach ( Client :: where ( 'is_deleted' , 0 ) -> where ( 'clients.updated_at' , '>' , now () -> subDays ( 2 )) -> cursor () as $client ) {
2022-05-15 09:51:06 +02:00
$invoice_balance = $client -> invoices () -> where ( 'is_deleted' , false ) -> whereIn ( 'status_id' , [ 2 , 3 ]) -> sum ( 'balance' );
2021-02-03 13:29:44 +01:00
$ledger = CompanyLedger :: where ( 'client_id' , $client -> id ) -> orderBy ( 'id' , 'DESC' ) -> first ();
2022-03-08 11:49:33 +01:00
if ( $ledger && number_format ( $ledger -> balance , 4 ) != number_format ( $client -> balance , 4 )) {
2021-06-21 00:18:30 +02:00
$this -> wrong_balances ++ ;
2022-03-08 11:49:33 +01:00
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " - Balance Failure - Client Balance = { $client -> balance } Ledger Balance = { $ledger -> balance } " );
2021-02-03 13:29:44 +01:00
$this -> isValid = false ;
2021-07-13 11:09:02 +02:00
2022-03-08 11:49:33 +01:00
if ( $this -> option ( 'ledger_balance' )){
2021-07-13 11:09:02 +02:00
$this -> logMessage ( " # { $client -> id } " . $client -> present () -> name . ' - ' . $client -> number . " Fixing { $client -> balance } to { $invoice_balance } " );
$client -> balance = $invoice_balance ;
$client -> save ();
$ledger -> adjustment = $invoice_balance ;
$ledger -> balance = $invoice_balance ;
$ledger -> notes = 'Ledger Adjustment' ;
$ledger -> save ();
}
2021-02-03 13:29:44 +01:00
}
}
2022-03-08 11:49:33 +01:00
$this -> logMessage ( " { $this -> wrong_balances } clients with incorrect ledger balances " );
2021-02-03 13:29:44 +01:00
}
2020-04-19 12:29:58 +02:00
private function checkLogoFiles ()
{
}
/**
* @ 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' ,
2021-07-06 09:22:12 +02:00
'recurring_invoice' ,
2020-06-28 00:24:08 +02:00
],
'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
}
2022-02-18 21:12:00 +01:00
public function checkAccountStatuses ()
{
Account :: where ( 'plan_expires' , '<=' , now () -> subDays ( 2 )) -> cursor () -> each ( function ( $account ){
2022-05-13 06:06:21 +02:00
$client = Client :: on ( 'db-ninja-01' ) -> where ( 'company_id' , config ( 'ninja.ninja_default_company_id' )) -> where ( 'custom_value2' , $account -> key ) -> first ();
if ( $client ){
$payment = Payment :: on ( 'db-ninja-01' )
-> where ( 'company_id' , config ( 'ninja.ninja_default_company_id' ))
-> where ( 'client_id' , $client -> id )
-> where ( 'date' , '>=' , now () -> subDays ( 2 ))
-> exists ();
if ( $payment )
$this -> logMessage ( " I found a payment for { $account -> key } " );
2022-02-18 21:12:00 +01:00
2022-05-13 06:06:21 +02:00
}
2022-02-18 21:12:00 +01:00
});
}
2022-07-27 09:37:37 +02:00
public function checkClientSettings ()
{
if ( $this -> option ( 'fix' ) == 'true' ) {
Client :: query () -> whereNull ( 'settings->currency_id' ) -> cursor () -> each ( function ( $client ){
if ( is_array ( $client -> settings ) && count ( $client -> settings ) == 0 )
{
$settings = ClientSettings :: defaults ();
$settings -> currency_id = $client -> company -> settings -> currency_id ;
}
else {
$settings = $client -> settings ;
$settings -> currency_id = $client -> company -> settings -> currency_id ;
}
$client -> settings = $settings ;
$client -> save ();
$this -> logMessage ( " Fixing currency for # { $client -> id } " );
});
Client :: query () -> whereNull ( 'country_id' ) -> cursor () -> each ( function ( $client ){
$client -> country_id = $client -> company -> settings -> country_id ;
$client -> save ();
$this -> logMessage ( " Fixing country for # { $client -> id } " );
});
}
}
2022-05-25 08:34:43 +02:00
public function checkBalanceVsPaidStatus ()
{
$this -> wrong_paid_status = 0 ;
foreach ( Invoice :: with ([ 'payments' ]) -> whereHas ( 'payments' ) -> where ( 'status_id' , 4 ) -> where ( 'balance' , '>' , 0 ) -> where ( 'is_deleted' , 0 ) -> cursor () as $invoice )
{
2022-06-19 05:01:29 +02:00
$this -> wrong_paid_status ++ ;
2022-05-25 08:34:43 +02:00
$this -> logMessage ( " # { $invoice -> id } " . ' - ' . $invoice -> number . " - Marked as paid, but balance = { $invoice -> balance } " );
if ( $this -> option ( 'balance_status' )){
$val = $invoice -> balance ;
$invoice -> balance = 0 ;
$invoice -> paid_to_date = $val ;
$invoice -> save ();
$p = $invoice -> payments -> first ();
if ( $p && ( int ) $p -> amount == 0 )
{
$p -> amount = $val ;
$p -> applied = $val ;
$p -> save ();
$pivot = $p -> paymentables -> first ();
$pivot -> amount = $val ;
$pivot -> save ();
}
$this -> logMessage ( " Fixing { $invoice -> id } settings payment to { $val } " );
}
}
$this -> logMessage ( $this -> wrong_paid_status . " wrong invoices with bad balance state " );
}
2022-05-13 06:06:21 +02:00
}