1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-14 15:13:29 +01:00
invoiceninja/app/Console/Commands/CheckData.php

1101 lines
38 KiB
PHP
Raw Normal View History

<?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
*/
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;
use App\Factory\VendorContactFactory;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
2021-07-01 12:58:38 +02:00
use App\Models\Company;
use App\Models\CompanyLedger;
2022-09-11 14:03:36 +02:00
use App\Models\CompanyUser;
use App\Models\Contact;
use App\Models\Credit;
2021-11-03 10:37:17 +01:00
use App\Models\CreditInvitation;
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;
use App\Models\Vendor;
use App\Utils\Ninja;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
2021-07-01 12:58:38 +02:00
use Illuminate\Support\Str;
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-09-05 00:37:11 +02:00
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=}';
/**
* @var string
*/
protected $description = 'Check/fix data';
protected $log = '';
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;
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}");
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();
$this->checkContacts();
$this->checkVendorContacts();
$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();
$this->checkVendorSettings();
2022-12-23 11:45:33 +01:00
$this->checkClientSettings();
2022-09-05 00:37:11 +02:00
if(Ninja::isHosted()){
2022-02-18 21:12:00 +01:00
$this->checkAccountStatuses();
2022-09-05 00:37:11 +02:00
$this->checkNinjaPortalUrls();
}
if (! $this->option('client_id')) {
$this->checkOAuth();
}
$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));
$errorEmail = config('ninja.error_email');
2022-07-27 09:37:37 +02:00
if (strlen($errorEmail) > 1) {
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'))
->subject('Check-Data: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)." [{$database}]");
});
} elseif (! $this->isValid) {
2020-10-21 05:10:32 +02:00
new Exception("Check data failed!!\n".$this->log);
}
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
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}");
});
}
}
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($users->count().' users with duplicate oauth ids');
if ($users->count() > 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,
]);
}
}
}
}
private function checkContacts()
{
// check for contacts with the contact_key value set
$contacts = DB::table('client_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('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')),
]);
}
}
// check for missing contacts
$clients = DB::table('clients')
->leftJoin('client_contacts', function ($join) {
$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']);
$this->logMessage($clients->count().' clients without any contacts');
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();
}
}
}
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') {
$vendors = Vendor::withTrashed()->doesntHave('contacts')->get();
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();
}
}
}
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;
}
$this->logMessage($count.' failed jobs');
}
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']);
$this->logMessage($invoices->count().' invoices without any invitations');
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)->first()->id;
$invitation->invitation_key = Str::random(config('ninja.key_length'));
$invitation->save();
}
}
}
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]);
$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");
})
->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-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-09-05 05:12:47 +02: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')){
2022-09-05 05:12:47 +02:00
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->paid_to_date} to {$total_paid_to_date}");
2021-11-01 12:15:56 +01:00
$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;
$credit_total_applied = 0;
2020-07-02 07:25:34 +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
$total_invoice_payments = 0;
2021-06-30 10:28:30 +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();
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;
2021-02-11 12:37:58 +01:00
// 10/02/21
foreach ($client->payments as $payment) {
$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;
}
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
2022-09-05 05:12:47 +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')){
2022-09-05 05:12:47 +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
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();
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
2022-09-05 05:12:47 +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
});
2021-06-21 00:18:30 +02:00
$this->logMessage("{$this->wrong_balances} clients with incorrect invoice balances");
}
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;
}
private function checkClientBalances()
{
2021-06-21 00:18:30 +02:00
$this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0;
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++;
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-09-05 05:12:47 +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-09-05 05:12:47 +02:00
$this->logMessage("# {$client_object->id} " . $client_object->present()->name().' - '.$client_object->number." Fixing {$client_object->balance} to " . $client['invoice_balance']);
2022-05-13 06:06:21 +02:00
$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;
}
2021-10-31 07:01:51 +01:00
}
2021-06-21 00:18:30 +02:00
$this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect client balances");
}
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){
2022-10-02 02:00:32 +02:00
//factor in over payments to the client balance
$over_payment = Payment::where('client_id', $client->id)
->where('is_deleted', 0)
->whereIn('status_id', [1,4])
->selectRaw('sum(amount - applied) as p')
->pluck('p')
->first();
2022-10-13 11:28:50 +02:00
$over_payment = $over_payment*-1;
2022-10-02 02:00:32 +02:00
2022-10-14 12:19:41 +02:00
if(floatval($over_payment) == floatval($client->balance)){
}
else {
$this->logMessage("# {$client->id} # {$client->name} {$client->balance} is invalid should be {$over_payment}");
}
2022-07-17 07:26:35 +02:00
2022-10-02 02:00:32 +02:00
if($this->option('client_balance') && (floatval($over_payment) != floatval($client->balance) )){
2022-07-17 07:26:35 +02:00
2022-09-05 05:12:47 +02:00
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to 0");
2022-07-17 07:26:35 +02:00
2022-10-13 11:28:50 +02:00
$client->balance = $over_payment;
2022-07-17 07:26:35 +02:00
$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-09-05 05:12:47 +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')){
2022-09-05 05:12:47 +02:00
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
2021-11-03 10:37:17 +01:00
$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
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-09-05 05:12:47 +02: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
2022-09-05 05:12:47 +02:00
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
2021-07-13 11:09:02 +02:00
$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
}
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;
$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-06-28 00:24:08 +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') {
2022-12-23 11:45:33 +01:00
// Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){
2022-07-27 09:37:37 +02:00
2022-12-23 11:45:33 +01:00
// 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;
// }
2022-07-27 09:37:37 +02:00
2022-12-23 11:45:33 +01:00
// $client->settings = $settings;
// $client->save();
2022-07-27 09:37:37 +02:00
2022-12-23 11:45:33 +01:00
// $this->logMessage("Fixing currency for # {$client->id}");
2022-07-27 09:37:37 +02:00
2022-12-23 11:45:33 +01:00
// });
2022-07-27 09:37:37 +02:00
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}");
});
}
}
public function checkVendorSettings()
{
if ($this->option('fix') == 'true')
{
Vendor::query()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor){
$vendor->currency_id = $vendor->company->settings->currency_id;
$vendor->save();
$this->logMessage("Fixing vendor currency for # {$vendor->id}");
});
}
}
2022-05-25 08:34:43 +02:00
public function checkBalanceVsPaidStatus()
{
$this->wrong_paid_status = 0;
2022-09-01 09:37:55 +02:00
foreach(Invoice::with(['payments'])->where('is_deleted',0)->where('balance', '>', 0)->whereHas('payments')->where('status_id', 4)->cursor() as $invoice)
2022-05-25 08:34:43 +02:00
{
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-09-05 00:37:11 +02:00
public function checkNinjaPortalUrls()
{
$wrong_count = CompanyUser::where('is_owner',1)->where('ninja_portal_url', '')->count();
$this->logMessage("Missing ninja portal Urls = {$wrong_count}");
if(!$this->option('portal_url'))
return;
CompanyUser::where('is_owner',1)->where('ninja_portal_url', '')->cursor()->each(function ($cu){
$cc = ClientContact::on('db-ninja-01')->where('company_id', config('ninja.ninja_default_company_id'))->where('email', $cu->user->email)->first();
if($cc){
$ninja_portal_url = "https://invoiceninja.invoicing.co/client/ninja/{$cc->contact_key}/{$cu->company->company_key}";
$cu->ninja_portal_url = $ninja_portal_url;
$cu->save();
$this->logMessage("Fixing - {$ninja_portal_url}");
}
2022-10-27 05:47:40 +02:00
else{
$c = Client::on('db-ninja-01')->where("company_id", config('ninja.ninja_default_company_id'))->where('custom_value2', $cu->account->key)->first();
if($c)
{
$cc = $c->contacts()->first();
if($cc)
{
$ninja_portal_url = "https://invoiceninja.invoicing.co/client/ninja/{$cc->contact_key}/{$cu->account->key}";
$cu->ninja_portal_url = $ninja_portal_url;
$cu->save();
$this->logMessage("Fixing - {$ninja_portal_url}");
}
}
}
2022-09-05 00:37:11 +02:00
});
}
2022-05-13 06:06:21 +02:00
}