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

1165 lines
41 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
*
2024-04-12 06:15:41 +02:00
* @copyright Copyright (c) 2024. 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;
2023-10-26 04:57:44 +02:00
use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory;
use App\Jobs\Company\CreateCompanyToken;
use App\Libraries\MultiDB;
use App\Models\Account;
2023-10-26 04:57:44 +02:00
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
2021-07-01 12:58:38 +02:00
use App\Models\Company;
2023-10-26 04:57:44 +02:00
use App\Models\CompanyLedger;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Contact;
2023-10-26 04:57:44 +02:00
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Invoice;
2023-10-26 04:57:44 +02:00
use App\Models\InvoiceInvitation;
2020-11-25 10:21:26 +01:00
use App\Models\Payment;
2023-01-20 05:17:21 +01:00
use App\Models\PurchaseOrder;
2023-10-26 04:57:44 +02:00
use App\Models\Quote;
use App\Models\QuoteInvitation;
2023-08-17 00:58:10 +02:00
use App\Models\RecurringInvoice;
2023-10-26 04:57:44 +02:00
use App\Models\RecurringInvoiceInvitation;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\Ninja;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
2023-10-26 04:57:44 +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
*/
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=} {--bank_transaction=}';
/**
* @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()
{
2023-02-16 02:36:09 +01:00
$time_start = microtime(true);
2021-06-27 06:52:15 +02:00
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]);
}
2023-02-16 02:36:09 +01: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();
$this->checkCompanyTokens();
2023-01-27 01:39:27 +01:00
$this->checkUserState();
2023-05-28 11:38:16 +02:00
$this->checkContactEmailAndSendEmailStatus();
2023-12-03 22:16:16 +01:00
$this->checkPaymentCurrency();
$this->checkSubdomainsSet();
2023-05-28 11:38:16 +02:00
2023-02-16 02:36:09 +01: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();
}
if($this->option('bank_transaction')) {
$this->fixBankTransactions();
}
$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) {
2023-07-06 02:12:42 +02:00
new \Exception("Check data failed!!".$this->log);
}
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
private function checkCompanyTokens()
{
CompanyUser::query()->cursor()->each(function ($cu) {
2024-03-25 02:23:20 +01:00
if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) {
2024-06-14 09:09:44 +02:00
2023-03-18 08:24:56 +01:00
if ($cu->company && $cu->user) {
2024-03-25 02:23:20 +01:00
$this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}");
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
2024-06-14 09:09:44 +02:00
}
2024-03-25 02:23:20 +01:00
if (!$cu->user) {
$this->logMessage("No user found for company user - removing company user");
$cu->forceDelete();
2023-08-08 11:11:59 +02:00
}
2024-06-14 09:09:44 +02:00
}
});
}
2024-01-14 05:05:00 +01:00
2023-03-13 21:25:27 +01:00
/**
* checkOauthSanity
2023-03-18 08:24:56 +01:00
*
2023-03-13 21:25:27 +01:00
* @return void
*/
2022-08-29 09:07:20 +02:00
private function checkOauthSanity()
{
2023-02-16 02:36:09 +01:00
User::where('oauth_provider_id', '1')->cursor()->each(function ($user) {
2022-08-29 09:07:20 +02:00
$this->logMessage("Invalid provider ID for user id# {$user->id}");
});
}
2022-08-28 03:08:15 +02:00
private function checkDuplicateRecurringInvoices()
{
2023-02-16 02:36:09 +01:00
if (Ninja::isHosted()) {
2022-08-28 03:08:15 +02:00
$c = Client::on('db-ninja-01')->where('company_id', config('ninja.ninja_default_company_id'))
->with('recurring_invoices')
->cursor()
2023-02-16 02:36:09 +01:00
->each(function ($client) {
if ($client->recurring_invoices()->where('is_deleted', 0)->where('deleted_at', null)->count() > 1) {
2024-05-04 23:36:54 +02:00
$this->logMessage("Duplicate Recurring Invoice => {$client->custom_value1} || {$client->id}}");
2023-02-16 02:36:09 +01:00
}
2022-08-28 03:08:15 +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($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}");
2024-01-14 05:05:00 +01:00
2021-06-05 07:58:37 +02:00
$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->saveQuietly();
}
}
}
private function checkVendorContacts()
{
// check for contacts with the contact_key value set
$contacts = DB::table('vendor_contacts')
->whereNull('contact_key')
->orderBy('id')
->get(['id']);
2023-12-03 22:16:16 +01:00
$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')),
]);
}
}
2024-01-14 05:05:00 +01:00
2023-12-03 22:16:16 +01:00
$vendors = Vendor::withTrashed()->doesntHave('contacts');
2024-01-14 05:05:00 +01:00
$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}");
2024-01-14 05:05:00 +01:00
$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->saveQuietly();
}
}
}
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;
2023-04-26 08:55:49 +02:00
$invitation->client_contact_id = ClientContact::whereClientId($invoice->client_id)->first()->id;
$invitation->key = Str::random(config('ninja.key_length'));
$invitation->saveQuietly();
}
}
}
2023-01-27 01:39:27 +01:00
private function checkUserState()
{
User::withTrashed()
->where('deleted_at', '0000-00-00 00:00:00.000000')
->cursor()
2023-02-16 02:36:09 +01:00
->each(function ($user) {
2023-01-27 01:39:27 +01:00
$user->restore();
});
}
private function checkEntityInvitations()
{
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01: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]);
CreditInvitation::where('deleted_at', "0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]);
2023-10-26 04:57:44 +02:00
InvoiceInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
2023-06-08 15:10:51 +02:00
$ii->sent_date = null;
$ii->saveQuietly();
});
InvoiceInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
$ii->viewed_date = null;
$ii->saveQuietly();
});
QuoteInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
$ii->sent_date = null;
$ii->saveQuietly();
});
QuoteInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
$ii->viewed_date = null;
$ii->saveQuietly();
});
2024-01-14 05:05:00 +01:00
2023-06-08 15:10:51 +02:00
CreditInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
$ii->sent_date = null;
$ii->saveQuietly();
});
CreditInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) {
$ii->viewed_date = null;
$ii->saveQuietly();
});
collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class, RecurringInvoice::class])->each(function ($entity) {
2023-02-16 02:36:09 +01:00
if ($entity::doesntHave('invitations')->count() > 0) {
2023-01-20 05:17:21 +01:00
$entity::doesntHave('invitations')->cursor()->each(function ($entity) {
$client_vendor_key = 'client_id';
$contact_id = 'client_contact_id';
$contact_class = ClientContact::class;
2023-04-26 08:55:49 +02:00
$entity_key = \Illuminate\Support\Str::of(class_basename($entity))->snake()->append('_id')->toString();
2023-01-20 05:17:21 +01:00
$entity_obj = get_class($entity).'Invitation';
2023-02-16 02:36:09 +01:00
if ($entity instanceof PurchaseOrder) {
2023-01-20 05:17:21 +01:00
$client_vendor_key = 'vendor_id';
$contact_id = 'vendor_contact_id';
$contact_class = VendorContact::class;
}
2023-02-16 02:36:09 +01:00
$invitation = false;
//check contact exists!
if ($contact_class::where('company_id', $entity->company_id)->where($client_vendor_key, $entity->{$client_vendor_key})->exists()) {
$contact = $contact_class::where('company_id', $entity->company_id)->where($client_vendor_key, $entity->{$client_vendor_key})->first();
//double check if an archived invite exists
if ($contact && $entity_obj::withTrashed()->where($entity_key, $entity->id)->where($contact_id, $contact->id)->count() != 0) {
$i = $entity_obj::withTrashed()->where($entity_key, $entity->id)->where($contact_id, $contact->id)->first();
$i->restore();
$this->logMessage("Found a valid contact and invitation restoring for {$entity_key} - {$entity->id}");
} else {
$invitation = new $entity_obj();
$invitation->company_id = $entity->company_id;
$invitation->user_id = $entity->user_id;
$invitation->{$entity_key} = $entity->id;
$invitation->{$contact_id} = $contact->id;
$invitation->key = Str::random(config('ninja.key_length'));
$this->logMessage("Add invitation for {$entity_key} - {$entity->id}");
}
2023-02-16 02:36:09 +01:00
} else {
$this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}");
2024-06-14 09:09:44 +02:00
try {
$entity->service()->createInvitations()->save();
2024-06-14 09:09:44 +02:00
} catch(\Exception $e) {
2024-03-25 02:23:20 +01:00
}
2024-06-14 09:09:44 +02:00
2023-02-16 02:36:09 +01:00
}
2023-02-16 02:36:09 +01:00
try {
if ($invitation) {
$invitation->save();
2023-02-16 02:36:09 +01:00
}
} catch(\Exception $e) {
2023-01-20 05:17:21 +01:00
$this->logMessage($e->getMessage());
$invitation = null;
}
});
}
});
}
private function fixInvitations($entities, $entity)
{
$entity_key = "{$entity}_id";
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
2023-02-16 02:36:09 +01:00
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
2023-02-16 02:36:09 +01:00
try {
2021-09-08 05:45:10 +02:00
$invitation->save();
2023-02-16 02:36:09 +01:00
} catch(\Exception $e) {
2021-09-08 05:45:10 +02:00
$invitation = null;
}
}
}
2021-11-01 12:15:56 +01:00
private function clientPaidToDateQuery()
{
2023-09-04 02:27:28 +02:00
$results = \DB::select("
2021-11-01 12:15:56 +01:00
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;
2023-09-04 02:27:28 +02:00
");
2024-01-14 05:05:00 +01:00
2021-11-01 12:15:56 +01:00
return $results;
}
private function clientCreditPaymentables($client)
{
2023-09-04 02:27:28 +02:00
$results = \DB::select("
2021-11-01 12:15:56 +01:00
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 = ?;
2023-09-04 02:27:28 +02:00
", [App\Models\Credit::class, $client->id]);
2024-01-14 05:05:00 +01:00
2021-11-01 12:15:56 +01:00
return $results;
}
private function checkPaidToDatesNew()
{
$clients_to_check = $this->clientPaidToDateQuery();
$this->wrong_paid_to_dates = 0;
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01:00
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
2023-02-16 02:36:09 +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;
2023-02-16 02:36:09 +01: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_paid_to_date}");
2021-11-01 12:15:56 +01:00
$client->paid_to_date = $total_paid_to_date;
$client->saveQuietly();
2021-11-01 12:15:56 +01:00
}
}
}
2021-11-05 05:24:37 +01:00
$this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect paid to dates");
2023-10-26 04:57:44 +02:00
}
2021-10-31 07:01:51 +01:00
private function clientBalanceQuery()
{
2023-09-04 02:27:28 +02:00
$results = \DB::select("
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;
2023-09-04 02:27:28 +02:00
");
2024-01-14 05:05:00 +01:00
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
2023-02-16 02:36:09 +01:00
foreach ($clients as $client) {
2021-11-01 12:15:56 +01:00
$client = (array)$client;
2024-01-14 05:05:00 +01:00
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'));
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01:00
if ($this->option('client_balance')) {
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'];
$client_object->saveQuietly();
2021-10-31 07:01:51 +01:00
}
2021-07-13 11:09:02 +02:00
2023-02-16 02:36:09 +01:00
$this->isValid = false;
}
}
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()
2023-02-16 02:36:09 +01:00
->where('is_deleted', false)
2022-07-17 07:26:35 +02:00
->where('balance', '!=', 0)
->cursor()
2023-02-16 02:36:09 +01:00
->each(function ($client) {
$count = Invoice::withTrashed()
->where('client_id', $client->id)
->where('is_deleted', false)
->whereIn('status_id', [2,3])
->count();
if ($count == 0) {
2024-01-14 05:05:00 +01:00
$this->isValid = false;
2023-12-03 22:16:16 +01:00
2023-02-16 02:36:09 +01: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();
2024-01-14 05:05:00 +01:00
$over_payment = $over_payment * -1;
2023-02-16 02:36:09 +01: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
2023-02-16 02:36:09 +01:00
if ($this->option('client_balance') && (floatval($over_payment) != floatval($client->balance))) {
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to 0");
2022-07-17 07:26:35 +02:00
2023-02-16 02:36:09 +01:00
$client->balance = $over_payment;
$client->saveQuietly();
2023-02-16 02:36:09 +01:00
}
}
2022-07-17 07:26:35 +02:00
});
}
2021-11-03 10:37:17 +01:00
private function invoiceBalanceQuery()
{
2023-09-04 02:27:28 +02:00
$results = \DB::select("
2021-11-03 10:37:17 +01:00
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;
2023-09-04 02:27:28 +02:00
");
2024-01-14 05:05:00 +01:00
2021-11-03 10:37:17 +01:00
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();
2023-02-16 02:36:09 +01:00
foreach ($_clients as $_client) {
2021-11-03 10:37:17 +01:00
$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)) {
2023-12-03 22:16:16 +01:00
$this->isValid = false;
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
2023-02-16 02:36:09 +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->saveQuietly();
2022-05-13 06:06:21 +02:00
}
2023-02-16 02:36:09 +01: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();
}
}
}
$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::query()->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
2023-02-16 02:36:09 +01:00
if ($this->option('ledger_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-07-13 11:09:02 +02:00
$client->balance = $invoice_balance;
$client->saveQuietly();
2021-07-13 11:09:02 +02:00
$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")
2023-09-06 01:10:59 +02:00
->whereRaw("{$table}.{$company_id} != {$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
}
}
}
}
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()
{
2023-02-16 02:36:09 +01:00
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();
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01:00
if ($client) {
2022-05-13 06:06:21 +02:00
$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();
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01:00
if ($payment) {
2023-12-03 22:16:16 +01:00
$this->isValid = false;
2022-05-13 06:06:21 +02:00
$this->logMessage("I found a payment for {$account->key}");
2023-02-16 02:36:09 +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('country_id')->orWhere('country_id', 0)->cursor()->each(function ($client) {
2022-07-27 09:37:37 +02:00
$client->country_id = $client->company->settings->country_id;
$client->saveQuietly();
2022-07-27 09:37:37 +02:00
$this->logMessage("Fixing country for # {$client->id}");
});
2024-01-24 04:45:18 +01:00
2024-03-12 01:30:05 +01:00
Client::query()->whereNull("settings->currency_id")->orWhereJsonContains('settings', ['currency_id' => ''])->cursor()->each(function ($client) {
2024-01-24 04:45:18 +01:00
$settings = $client->settings;
$settings->currency_id = (string)$client->company->settings->currency_id;
$client->settings = $settings;
$client->saveQuietly();
$this->logMessage("Fixing currency_id for # {$client->id}");
});
2024-02-13 05:25:18 +01:00
Payment::withTrashed()->where('exchange_rate', 0)->cursor()->each(function ($payment) {
2024-01-24 04:45:18 +01:00
$payment->exchange_rate = 1;
$payment->saveQuietly();
$this->logMessage("Fixing exchange rate for # {$payment->id}");
});
Payment::withTrashed()
->whereHas("client", function ($query) {
$query->whereColumn("settings->currency_id", "!=", "payments.currency_id");
})
->cursor()
->each(function ($p) {
$p->currency_id = $p->client->settings->currency_id;
$p->saveQuietly();
2024-02-13 05:25:18 +01:00
2024-01-24 04:58:06 +01:00
$this->logMessage("Fixing currency for # {$p->id}");
});
2024-02-13 05:25:18 +01:00
2024-01-24 04:58:06 +01:00
Company::whereNull("subdomain")
->cursor()
->when(Ninja::isHosted())
->each(function ($c) {
$c->subdomain = MultiDB::randomSubdomainGenerator();
$c->save();
$this->logMessage("Fixing subdomain for # {$c->id}");
2024-01-24 04:45:18 +01:00
});
2024-02-01 05:45:21 +01:00
Invoice::withTrashed()
->where("partial", 0)
->whereNotNull("partial_due_date")
->cursor()
->each(function ($i) {
$i->partial_due_date = null;
$i->saveQuietly();
2024-02-13 05:25:18 +01:00
2024-02-01 05:45:21 +01:00
$this->logMessage("Fixing partial due date for # {$i->id}");
});
2024-06-14 09:09:44 +02:00
Company::whereDoesntHave('company_users', function ($query) {
$query->where('is_owner', 1);
2024-02-28 09:04:06 +01:00
})
->cursor()
->when(Ninja::isHosted())
2024-06-14 09:09:44 +02:00
->each(function ($c) {
2024-02-28 09:04:06 +01:00
$this->logMessage("Orphan Account # {$c->account_id}");
});
CompanyUser::whereDoesntHave('tokens')
->cursor()
->when(Ninja::isHosted())
2024-06-14 09:09:44 +02:00
->each(function ($cu) {
2024-02-28 09:04:06 +01:00
$this->logMessage("Missing tokens for Company User # {$cu->id}");
2024-02-01 05:45:21 +01:00
2024-02-28 09:04:06 +01:00
});
CompanyUser::whereDoesntHave('user')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu) {
$this->logMessage("Missing user for Company User # {$cu->id}");
});
2024-02-01 05:45:21 +01:00
2024-03-13 00:36:41 +01:00
$cus = CompanyUser::withTrashed()
->whereHas("user", function ($query) {
$query->whereColumn("users.account_id", "!=", "company_user.account_id");
})->pluck('id')->implode(",");
$this->logMessage("Cross Linked CompanyUser ids # {$cus}");
2022-07-27 09:37:37 +02:00
}
}
public function checkVendorSettings()
{
2023-02-16 02:36:09 +01:00
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->saveQuietly();
$this->logMessage("Fixing vendor currency for # {$vendor->id}");
});
}
}
2022-05-25 08:34:43 +02:00
public function checkBalanceVsPaidStatus()
{
$this->wrong_paid_status = 0;
2023-02-16 02:36:09 +01:00
foreach (Invoice::with(['payments'])->where('is_deleted', 0)->where('balance', '>', 0)->whereHas('payments')->where('status_id', 4)->cursor() as $invoice) {
2022-06-19 05:01:29 +02:00
$this->wrong_paid_status++;
2024-01-14 05:05:00 +01:00
2022-05-25 08:34:43 +02:00
$this->logMessage("# {$invoice->id} " . ' - '.$invoice->number." - Marked as paid, but balance = {$invoice->balance}");
2023-02-16 02:36:09 +01:00
if ($this->option('balance_status')) {
2022-05-25 08:34:43 +02:00
$val = $invoice->balance;
$invoice->balance = 0;
2024-01-14 05:05:00 +01:00
$invoice->paid_to_date = $val;
$invoice->saveQuietly();
2022-05-25 08:34:43 +02:00
$p = $invoice->payments->first();
2023-02-16 02:36:09 +01:00
if ($p && (int)$p->amount == 0) {
2022-05-25 08:34:43 +02:00
$p->amount = $val;
$p->applied = $val;
$p->saveQuietly();
2022-05-25 08:34:43 +02:00
$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()
{
2023-02-16 02:36:09 +01:00
$wrong_count = CompanyUser::where('is_owner', 1)->where('ninja_portal_url', '')->count();
2022-09-05 00:37:11 +02:00
$this->logMessage("Missing ninja portal Urls = {$wrong_count}");
2023-02-16 02:36:09 +01:00
if (!$this->option('portal_url')) {
2022-09-05 00:37:11 +02:00
return;
2023-02-16 02:36:09 +01:00
}
2022-09-05 00:37:11 +02:00
2023-02-16 02:36:09 +01:00
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();
2022-09-05 00:37:11 +02:00
2023-02-16 02:36:09 +01:00
if ($cc) {
2023-11-24 00:23:40 +01:00
$ninja_portal_url = config('ninja.ninja_client_portal')."/client/ninja/{$cc->contact_key}/{$cu->account->key}";
2022-09-05 00:37:11 +02:00
$cu->ninja_portal_url = $ninja_portal_url;
$cu->save();
$this->logMessage("Fixing - {$ninja_portal_url}");
2023-02-16 02:36:09 +01:00
} else {
2022-10-27 05:47:40 +02:00
$c = Client::on('db-ninja-01')->where("company_id", config('ninja.ninja_default_company_id'))->where('custom_value2', $cu->account->key)->first();
2023-02-16 02:36:09 +01:00
if ($c) {
$cc = $c->contacts()->first();
2024-01-14 05:05:00 +01:00
2023-02-16 02:36:09 +01:00
if ($cc) {
2023-11-24 00:23:40 +01:00
$ninja_portal_url = config('ninja.ninja_client_portal')."/client/ninja/{$cc->contact_key}/{$cu->account->key}";
2022-10-27 05:47:40 +02:00
$cu->ninja_portal_url = $ninja_portal_url;
$cu->save();
$this->logMessage("Fixing - {$ninja_portal_url}");
}
2023-02-16 02:36:09 +01:00
}
2022-10-27 05:47:40 +02:00
}
2022-09-05 00:37:11 +02:00
});
}
public function fixBankTransactions()
{
$this->logMessage("checking bank transactions");
2023-10-26 04:57:44 +02:00
BankTransaction::with('payment')->withTrashed()->where('invoice_ids', ',,,,,,,,')->cursor()->each(function ($bt) {
if($bt->payment->exists()) {
2023-12-03 22:16:16 +01:00
$this->isValid = false;
$bt->invoice_ids = collect($bt->payment->invoices)->pluck('hashed_id')->implode(',');
$bt->save();
$this->logMessage("Fixing - {$bt->id}");
2024-01-14 05:05:00 +01:00
}
});
}
2023-05-28 11:38:16 +02:00
public function checkContactEmailAndSendEmailStatus()
{
$q = ClientContact::whereNull('email')
->where('send_email', true);
$this->logMessage($q->count() . " Contacts with Send Email = true but no email address");
if ($this->option('fix') == 'true') {
2023-10-26 04:57:44 +02:00
$q->cursor()->each(function ($c) {
2023-05-28 11:38:16 +02:00
$c->send_email = false;
$c->saveQuietly();
$this->logMessage("Fixing - {$c->id}");
});
}
}
2023-12-03 22:16:16 +01:00
public function checkSubdomainsSet()
{
2024-01-14 05:05:00 +01:00
if(Ninja::isSelfHost()) {
return;
2024-01-14 05:05:00 +01:00
}
Company::query()
->whereNull('subdomain')
->orWhere('subdomain', '')
->cursor()
2024-01-14 05:05:00 +01:00
->each(function ($c) {
$c->subdomain = MultiDB::randomSubdomainGenerator();
$c->save();
2024-01-14 05:05:00 +01:00
});
}
2023-12-03 22:16:16 +01:00
public function checkPaymentCurrency()
{
2024-01-14 05:05:00 +01:00
$p = Payment::with('company', 'client')
2023-12-03 22:16:16 +01:00
->withTrashed()
->where('currency_id', '')
->orWhereNull('currency_id');
$this->logMessage($p->count() . " Payments with No currency set");
2024-01-14 05:05:00 +01:00
if($p->count() != 0) {
2023-12-03 22:16:16 +01:00
$this->isValid = false;
2024-01-14 05:05:00 +01:00
}
2023-12-03 22:16:16 +01:00
if ($this->option('fix') == 'true') {
$p->cursor()->each(function ($c) {
$c->currency_id = $c->client->settings->currency_id ?? $c->company->settings->currency_id;
$c->saveQuietly();
$this->logMessage("Fixing - {$c->id}");
});
}
}
2023-02-16 02:36:09 +01:00
}