Merge pull request #7987 from turbo124/v5-develop

Refactor the way we execute scheduled commands
This commit is contained in:
David Bomba 2022-11-27 09:26:06 +11:00 committed by GitHub
commit faf4b312d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 347 additions and 237 deletions

View File

@ -54,13 +54,13 @@ class Kernel extends ConsoleKernel
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer(); $schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* Returns the number of jobs in the queue */ /* Returns the number of jobs in the queue */
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping(); $schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping()->name('queue-size-job')->onOneServer();
/* Checks for large companies and marked them as is_large */ /* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck)->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer(); $schedule->job(new CompanySizeCheck)->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();
/* Pulls in the latest exchange rates */ /* Pulls in the latest exchange rates */
$schedule->job(new UpdateExchangeRates)->dailyAt('23:30')->withoutOverlapping(); $schedule->job(new UpdateExchangeRates)->dailyAt('23:30')->withoutOverlapping()->name('exchange-rate-job')->onOneServer();
/* Runs cleanup code for subscriptions */ /* Runs cleanup code for subscriptions */
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer(); $schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
@ -105,11 +105,11 @@ class Kernel extends ConsoleKernel
//not used @deprecate //not used @deprecate
// $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping(); // $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily('02:00')->withoutOverlapping(); $schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:05')->withoutOverlapping(); $schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping(); $schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
} }

View File

@ -59,7 +59,7 @@ class BankTransformer extends BaseTransformer
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'CREDIT') || strtolower($transaction['transaction.base_type']) == 'deposit')) if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'CREDIT') || strtolower($transaction['transaction.base_type']) == 'deposit'))
return 'CREDIT'; return 'CREDIT';
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.bank_type']) == 'withdrawal')) if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.base_type']) == 'withdrawal'))
return 'DEBIT'; return 'DEBIT';
if(array_key_exists('transaction.category_id', $transaction)) if(array_key_exists('transaction.category_id', $transaction))

View File

@ -46,19 +46,7 @@ class RecurringExpensesCron
nlog('Sending recurring expenses '.Carbon::now()->format('Y-m-d h:i:s')); nlog('Sending recurring expenses '.Carbon::now()->format('Y-m-d h:i:s'));
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled')) {
$this->getRecurringExpenses();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->getRecurringExpenses();
}
}
}
private function getRecurringExpenses()
{
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString()) $recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date') ->whereNotNull('next_send_date')
->whereNull('deleted_at') ->whereNull('deleted_at')
@ -79,6 +67,41 @@ class RecurringExpensesCron
$this->generateExpense($recurring_expense); $this->generateExpense($recurring_expense);
} }
}); });
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();
nlog(now()->format('Y-m-d').' Generating Recurring Expenses. Count = '.$recurring_expenses->count());
$recurring_expenses->each(function ($recurring_expense, $key) {
nlog('Current date = '.now()->format('Y-m-d').' Recurring date = '.$recurring_expense->next_send_date);
if (! $recurring_expense->company->is_disabled) {
$this->generateExpense($recurring_expense);
}
});
}
}
}
private function getRecurringExpenses()
{
//extracting this back to the if/else block to test duplicate crons
} }
private function generateExpense(RecurringExpense $recurring_expense) private function generateExpense(RecurringExpense $recurring_expense)

View File

@ -41,19 +41,7 @@ class SubscriptionCron
nlog('Subscription Cron'); nlog('Subscription Cron');
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled')) {
$this->loopSubscriptions();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->loopSubscriptions();
}
}
}
private function loopSubscriptions()
{
$invoices = Invoice::where('is_deleted', 0) $invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0) ->where('balance', '>', 0)
@ -76,9 +64,39 @@ class SubscriptionCron
//This will send the notification daily. //This will send the notification daily.
//We'll need to handle this by performing some action on the invoice to either archive it or delete it? //We'll need to handle this by performing some action on the invoice to either archive it or delete it?
}); });
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
->whereNull('deleted_at')
->whereNotNull('subscription_id')
->cursor();
$invoices->each(function ($invoice) {
$subscription = $invoice->subscription;
$body = [
'context' => 'plan_expired',
'client' => $invoice->client->hashed_id,
'invoice' => $invoice->hashed_id,
'subscription' => $subscription->hashed_id,
];
$this->sendLoad($subscription, $body);
//This will send the notification daily.
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
});
}
}
} }
private function handleWebhook($invoice, $subscription)
{
}
} }

View File

@ -44,22 +44,13 @@ class BankTransactionSync implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
// if (! Ninja::isHosted()) {
// return;
// }
//multiDB environment, need to //multiDB environment, need to
foreach (MultiDB::$dbs as $db) { foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db); MultiDB::setDB($db);
nlog("syncing transactions"); nlog("syncing transactions");
$this->syncTransactions();
}
}
public function syncTransactions()
{
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){ $a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
// $queue = Ninja::isHosted() ? 'bank' : 'default'; // $queue = Ninja::isHosted() ? 'bank' : 'default';
@ -77,5 +68,6 @@ class BankTransactionSync implements ShouldQueue
}); });
} }
}
} }

View File

@ -42,20 +42,32 @@ class CompanySizeCheck implements ShouldQueue
public function handle() public function handle()
{ {
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled')) {
$this->check();
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
nlog("Marking company {$company->id} as large");
$company->account->companies()->update(['is_large' => true]);
}
});
nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay())
->cursor()
->each(function ($client){
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
});
} else { } else {
//multiDB environment, need to //multiDB environment, need to
foreach (MultiDB::$dbs as $db) { foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db); MultiDB::setDB($db);
$this->check(); nlog("Company size check db {$db}");
}
}
}
private function check()
{
nlog("Checking all company sizes");
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) { Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) { if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
@ -77,4 +89,7 @@ class CompanySizeCheck implements ShouldQueue
}); });
} }
}
}
} }

View File

@ -40,7 +40,7 @@ class QueueSize implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() :void
{ {
LightLogs::create(new QueueSizeAnalytic(Queue::size())) LightLogs::create(new QueueSizeAnalytic(Queue::size()))
->send(); ->send();

View File

@ -42,21 +42,8 @@ class QuoteCheckExpired implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if (! config('ninja.db.multi_db_enabled')) if (! config('ninja.db.multi_db_enabled')){
return $this->checkForExpiredQuotes();
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->checkForExpiredQuotes();
}
}
private function checkForExpiredQuotes()
{
Quote::query() Quote::query()
->where('status_id', Quote::STATUS_SENT) ->where('status_id', Quote::STATUS_SENT)
->where('is_deleted', false) ->where('is_deleted', false)
@ -75,6 +62,41 @@ class QuoteCheckExpired implements ShouldQueue
->each(function ($quote){ ->each(function ($quote){
$this->queueExpiredQuoteNotification($quote); $this->queueExpiredQuoteNotification($quote);
}); });
}
else {
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
Quote::query()
->where('status_id', Quote::STATUS_SENT)
->where('is_deleted', false)
->whereNull('deleted_at')
->whereNotNull('due_date')
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
// ->where('due_date', '<='. now()->toDateTimeString())
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
->cursor()
->each(function ($quote){
$this->queueExpiredQuoteNotification($quote);
});
}
}
}
private function checkForExpiredQuotes()
{
} }
private function queueExpiredQuoteNotification(Quote $quote) private function queueExpiredQuoteNotification(Quote $quote)

View File

@ -44,34 +44,43 @@ class ReminderJob implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() :void
{ {
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled'))
$this->processReminders(); {
set_time_limit(0);
nlog("Sending invoice reminders on ".now()->format('Y-m-d h:i:s'));
Invoice::query()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->cursor()->each(function ($invoice) {
$this->sendReminderForInvoice($invoice);
});
} else { } else {
//multiDB environment, need to //multiDB environment, need to
/*
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
*/
//24-11-2022 fix for potential memory leak during a long running process, the second reminder may run twice
foreach (config('ninja.dbs') as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
} foreach (MultiDB::$dbs as $db)
}
private function processReminders()
{ {
nlog('Sending invoice reminders '.now()->format('Y-m-d h:i:s'));
set_time_limit(0); MultiDB::setDB($db);
nlog("Sending invoice reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Invoice::query() Invoice::query()
->where('is_deleted', 0) ->where('is_deleted', 0)
@ -88,6 +97,18 @@ class ReminderJob implements ShouldQueue
}) })
->with('invitations')->cursor()->each(function ($invoice) { ->with('invitations')->cursor()->each(function ($invoice) {
// if ($invoice->refresh() && $invoice->isPayable()) { // if ($invoice->refresh() && $invoice->isPayable()) {
$this->sendReminderForInvoice($invoice);
});
}
}
}
private function sendReminderForInvoice($invoice) {
if ($invoice->isPayable()) { if ($invoice->isPayable()) {
//Attempts to prevent duplicates from sending //Attempts to prevent duplicates from sending
@ -115,7 +136,7 @@ class ReminderJob implements ShouldQueue
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) { (Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { $invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); EmailEntity::dispatchSync($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}"); nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
}); });
@ -129,7 +150,6 @@ class ReminderJob implements ShouldQueue
$invoice->save(); $invoice->save();
} }
});
} }
/** /**

View File

@ -34,19 +34,7 @@ class UpdateExchangeRates implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() :void
{
if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->updateCurrencies();
}
} else {
$this->updateCurrencies();
}
}
private function updateCurrencies()
{ {
info('updating currencies'); info('updating currencies');
@ -56,6 +44,10 @@ class UpdateExchangeRates implements ShouldQueue
$cc_endpoint = sprintf('https://openexchangerates.org/api/latest.json?app_id=%s', config('ninja.currency_converter_api_key')); $cc_endpoint = sprintf('https://openexchangerates.org/api/latest.json?app_id=%s', config('ninja.currency_converter_api_key'));
if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$client = new Client(); $client = new Client();
$response = $client->get($cc_endpoint); $response = $client->get($cc_endpoint);
@ -71,5 +63,28 @@ class UpdateExchangeRates implements ShouldQueue
$currencies = Currency::orderBy('name')->get(); $currencies = Currency::orderBy('name')->get();
Cache::forever('currencies', $currencies); Cache::forever('currencies', $currencies);
} }
} else {
$client = new Client();
$response = $client->get($cc_endpoint);
$currency_api = json_decode($response->getBody());
/* Update all currencies */
Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save();
});
/* Rebuild the cache */
$currencies = Currency::orderBy('name')->get();
Cache::forever('currencies', $currencies);
}
}
} }

View File

@ -785,7 +785,7 @@ class SubscriptionService
*/ */
public function triggerWebhook($context) public function triggerWebhook($context)
{ {
nlog("trigger webook"); nlog("trigger webhook");
if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) {
return ["message" => "Success", "status_code" => 200]; return ["message" => "Success", "status_code" => 200];
@ -901,6 +901,8 @@ class SubscriptionService
*/ */
public function handleCancellation(RecurringInvoice $recurring_invoice) public function handleCancellation(RecurringInvoice $recurring_invoice)
{ {
$invoice_start_date = false;
$refund_end_date = false;
//only refund if they are in the refund window. //only refund if they are in the refund window.
$outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id) $outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id)
@ -909,8 +911,11 @@ class SubscriptionService
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
if($outstanding_invoice)
{
$invoice_start_date = Carbon::parse($outstanding_invoice->date); $invoice_start_date = Carbon::parse($outstanding_invoice->date);
$refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period); $refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period);
}
/* Stop the recurring invoice and archive */ /* Stop the recurring invoice and archive */
$recurring_invoice->service()->stop()->save(); $recurring_invoice->service()->stop()->save();
@ -918,7 +923,7 @@ class SubscriptionService
$recurring_invoice_repo->archive($recurring_invoice); $recurring_invoice_repo->archive($recurring_invoice);
/* Refund only if we are in the window - and there is nothing outstanding on the invoice */ /* Refund only if we are in the window - and there is nothing outstanding on the invoice */
if($refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0) if($refund_end_date && $refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0)
{ {
if($outstanding_invoice->payments()->exists()) if($outstanding_invoice->payments()->exists())

View File

@ -25,7 +25,7 @@
<div class="mb-4"> <div class="mb-4">
@include('portal.ninja2020.quotes.includes.actions', ['quote' => $quote]) @include('portal.ninja2020.quotes.includes.actions', ['quote' => $quote])
</div> </div>
@elseif($quote->status_id === \App\Models\Quote::STATUS_CONVERTED) @elseif($quote->status_id == \App\Models\Quote::STATUS_CONVERTED)
<div class="bg-white shadow sm:rounded-lg mb-4"> <div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
@ -57,7 +57,7 @@
</div> </div>
</div> </div>
@elseif($quote->status_id === \App\Models\Quote::STATUS_APPROVED) @elseif($quote->status_id == \App\Models\Quote::STATUS_APPROVED)
<div class="bg-white shadow sm:rounded-lg mb-4"> <div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">

View File

@ -94,7 +94,7 @@
</div> </div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center"> <div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }"> <div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
<button class="button button-danger" translate @click="open = true">Request Cancellation <button class="button button-danger" translate @click="open = true">{{ ctrans('texts.request_cancellation') }}
</button> </button>
@include('portal.ninja2020.recurring_invoices.includes.modals.cancellation') @include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
</div> </div>