Merge pull request #8002 from turbo124/v5-stable

v5.5.43
This commit is contained in:
David Bomba 2022-11-29 22:29:23 +11:00 committed by GitHub
commit c122cfaa28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 61361 additions and 61134 deletions

View File

@ -1 +1 @@
5.5.42 5.5.43

View File

@ -41,6 +41,7 @@ use App\Models\Vendor;
use App\Models\VendorContact; use App\Models\VendorContact;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Carbon\Carbon; use Carbon\Carbon;
@ -53,7 +54,7 @@ use Illuminate\Support\Str;
class DemoMode extends Command class DemoMode extends Command
{ {
use MakesHash, GeneratesCounter; use MakesHash, GeneratesCounter, AppSetup;
protected $signature = 'ninja:demo-mode'; protected $signature = 'ninja:demo-mode';
@ -83,34 +84,14 @@ class DemoMode extends Command
$cached_tables = config('ninja.cached_tables'); $cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
$this->info('Migrating'); $this->info('Migrating');
Artisan::call('migrate:fresh --force'); Artisan::call('migrate:fresh --force');
$this->info('Seeding'); $this->info('Seeding');
Artisan::call('db:seed --force'); Artisan::call('db:seed --force');
$this->buildCache(true);
$this->info('Seeding Random Data'); $this->info('Seeding Random Data');
$this->createSmallAccount(); $this->createSmallAccount();

View File

@ -26,6 +26,7 @@ use App\Utils\Traits\MakesReminders;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
//@deprecated 27-11-2022 - only ever should be used for testing
class SendRemindersCron extends Command class SendRemindersCron extends Command
{ {
use MakesReminders, MakesDates; use MakesReminders, MakesDates;

View File

@ -48,46 +48,46 @@ class Kernel extends ConsoleKernel
$schedule->job(new VersionCheck)->daily(); $schedule->job(new VersionCheck)->daily();
/* Checks and cleans redundant files */ /* Checks and cleans redundant files */
$schedule->job(new DiskCleanup)->dailyAt('02:10')->withoutOverlapping(); $schedule->job(new DiskCleanup)->dailyAt('02:10')->withoutOverlapping()->name('disk-cleanup-job')->onOneServer();
/* Send reminders */ /* Send reminders */
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping(); $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(); $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(); $schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
/* Sends recurring invoices*/ /* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping(); $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Sends recurring invoices*/ /* Sends recurring invoices*/
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping(); $schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();
/* Fires notifications for expired Quotes */ /* Fires notifications for expired Quotes */
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping(); $schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
/* Performs auto billing */ /* Performs auto billing */
$schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping(); $schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer();
/* Checks the status of the scheduler */ /* Checks the status of the scheduler */
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping(); $schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
/* Checks for scheduled tasks */ /* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->dailyAt('06:50')->withoutOverlapping(); $schedule->job(new TaskScheduler())->dailyAt('06:50')->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Performs system maintenance such as pruning the backup table */ /* Performs system maintenance such as pruning the backup table */
$schedule->job(new SystemMaintenance)->weekly()->withoutOverlapping(); $schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
/* Pulls in bank transactions from third party services */ /* Pulls in bank transactions from third party services */
$schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping(); $schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
if (Ninja::isSelfHost()) { if (Ninja::isSelfHost()) {
@ -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

@ -185,7 +185,7 @@ class InvoiceFilters extends QueryFilters
* @param string sort formatted as column|asc * @param string sort formatted as column|asc
* @return Builder * @return Builder
*/ */
public function sort(string $sort) : Builder public function sort(string $sort = '') : Builder
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);

View File

@ -539,7 +539,7 @@ class BaseController extends Controller
$query->where('bank_integrations.user_id', $user->id); $query->where('bank_integrations.user_id', $user->id);
} }
}, },
'company.bank_transaction_rules'=> function ($query) use ($updated_at, $user) { 'company.bank_transaction_rules'=> function ($query) use ($user) {
if (! $user->isAdmin()) { if (! $user->isAdmin()) {
$query->where('bank_transaction_rules.user_id', $user->id); $query->where('bank_transaction_rules.user_id', $user->id);

View File

@ -428,13 +428,13 @@ class InvoiceController extends BaseController
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$transaction = [ // $transaction = [
'invoice' => $invoice->transaction_event(), // 'invoice' => $invoice->transaction_event(),
'payment' => [], // 'payment' => [],
'client' => $invoice->client->transaction_event(), // 'client' => $invoice->client->transaction_event(),
'credit' => [], // 'credit' => [],
'metadata' => [], // 'metadata' => [],
]; // ];
// TransactionLog::dispatch(TransactionEvent::INVOICE_UPDATED, $transaction, $invoice->company->db); // TransactionLog::dispatch(TransactionEvent::INVOICE_UPDATED, $transaction, $invoice->company->db);

View File

@ -44,7 +44,7 @@ class StoreBankIntegrationRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if(!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0) if(!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0 && array_key_exists('bank_account_name', $input))
$input['provider_name'] = $input['bank_account_name']; $input['provider_name'] = $input['bank_account_name'];
$this->replace($input); $this->replace($input);

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule; use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Models\Gateway; use App\Models\Gateway;
use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver; use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver;
use Illuminate\Validation\Rule;
class StoreCompanyGatewayRequest extends Request class StoreCompanyGatewayRequest extends Request
{ {
@ -33,7 +34,7 @@ class StoreCompanyGatewayRequest extends Request
public function rules() public function rules()
{ {
$rules = [ $rules = [
'gateway_key' => 'required|alpha_num', 'gateway_key' => ['required','alpha_num',Rule::exists('gateways','key')],
'fees_and_limits' => new ValidCompanyGatewayFeesAndLimitsRule(), 'fees_and_limits' => new ValidCompanyGatewayFeesAndLimitsRule(),
]; ];

View File

@ -32,8 +32,6 @@ class GenericReportRequest extends Request
return [ return [
'date_range' => 'bail|required|string', 'date_range' => 'bail|required|string',
// 'start_date' => [Rule::requiredIf($this->date_range === 'custom')],
// 'end_date' => [Rule::requiredIf($this->date_range === 'custom')],
'end_date' => 'bail|required_if:date_range,custom|nullable|date', 'end_date' => 'bail|required_if:date_range,custom|nullable|date',
'start_date' => 'bail|required_if:date_range,custom|nullable|date', 'start_date' => 'bail|required_if:date_range,custom|nullable|date',
'report_keys' => 'present|array', 'report_keys' => 'present|array',
@ -57,6 +55,12 @@ class GenericReportRequest extends Request
$input['send_email'] = true; $input['send_email'] = true;
} }
if (array_key_exists('date_range', $input) && $input['date_range'] != 'custom') {
$input['start_date'] = null;
$input['end_date'] = null;
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -24,7 +24,7 @@ class ConfirmSmsRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->isAdmin(); return true;
} }
public function rules() public function rules()

View File

@ -24,7 +24,7 @@ class GenerateSmsRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user(); return true;
} }

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

@ -36,6 +36,8 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use function Amp\call;
class CompanyExport implements ShouldQueue class CompanyExport implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
@ -531,6 +533,15 @@ class CompanyExport implements ShouldQueue
$path = 'backups'; $path = 'backups';
Storage::makeDirectory(public_path('storage/backups/'));
try {
mkdir(public_path('storage/backups/'));
}
catch(\Exception $e) {
nlog("could not create directory");
}
$zip_path = public_path('storage/backups/'.$file_name); $zip_path = public_path('storage/backups/'.$file_name);
$zip = new \ZipArchive(); $zip = new \ZipArchive();

View File

@ -62,8 +62,15 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count().' partial invoices to auto bill'); nlog($auto_bill_partial_invoices->count().' partial invoices to auto bill');
$auto_bill_partial_invoices->cursor()->each(function ($invoice) { $auto_bill_partial_invoices->chunk(100, function ($invoices) {
AutoBill::dispatch($invoice->id, false);
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
}); });
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now()) $auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
@ -79,8 +86,15 @@ class AutoBillCron
nlog($auto_bill_invoices->count().' full invoices to auto bill'); nlog($auto_bill_invoices->count().' full invoices to auto bill');
$auto_bill_invoices->cursor()->each(function ($invoice) { $auto_bill_invoices->chunk(100, function ($invoices) {
AutoBill::dispatch($invoice->id, false);
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
}); });
} else { } else {
//multiDB environment, need to //multiDB environment, need to
@ -100,8 +114,14 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count()." partial invoices to auto bill db = {$db}"); nlog($auto_bill_partial_invoices->count()." partial invoices to auto bill db = {$db}");
$auto_bill_partial_invoices->cursor()->each(function ($invoice) use ($db) { $auto_bill_partial_invoices->chunk(100, function ($invoices) use($db){
AutoBill::dispatch($invoice->id, $db);
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, $db);
}
sleep(2);
}); });
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now()) $auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
@ -117,10 +137,15 @@ class AutoBillCron
nlog($auto_bill_invoices->count()." full invoices to auto bill db = {$db}"); nlog($auto_bill_invoices->count()." full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use ($db) { $auto_bill_invoices->chunk(100, function ($invoices) use($db){
nlog($this->counter);
AutoBill::dispatch($invoice->id, $db); foreach($invoices as $invoice)
$this->counter++; {
AutoBill::dispatch($invoice->id, $db);
}
sleep(2);
}); });
} }

View File

@ -46,39 +46,62 @@ 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();
$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);
}
});
} 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->getRecurringExpenses(); $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() private function getRecurringExpenses()
{ {
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString()) //extracting this back to the if/else block to test duplicate crons
->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 generateExpense(RecurringExpense $recurring_expense) private function generateExpense(RecurringExpense $recurring_expense)

View File

@ -41,44 +41,62 @@ class SubscriptionCron
nlog('Subscription Cron'); nlog('Subscription Cron');
if (! config('ninja.db.multi_db_enabled')) { if (! config('ninja.db.multi_db_enabled')) {
$this->loopSubscriptions();
$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?
});
} 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->loopSubscriptions(); $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 loopSubscriptions()
{
$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

@ -99,11 +99,11 @@ class EmailEntity implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() :void
{ {
/* Don't fire emails if the company is disabled */ /* Don't fire emails if the company is disabled */
if ($this->company->is_disabled) { if ($this->company->is_disabled) {
return true; return;
} }
/* Set DB */ /* Set DB */

View File

@ -44,38 +44,30 @@ 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(); $a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
// $queue = Ninja::isHosted() ? 'bank' : 'default';
if($account->isPaid() && $account->plan == 'enterprise')
{
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration);
});
}
});
} }
} }
public function syncTransactions()
{
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
// $queue = Ninja::isHosted() ? 'bank' : 'default';
if($account->isPaid() && $account->plan == 'enterprise')
{
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration);
});
}
});
}
} }

View File

@ -42,39 +42,54 @@ 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}");
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();
});
} }
} }
} }
private function check()
{
nlog("Checking all company sizes");
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();
});
}
} }

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

@ -49,10 +49,6 @@ class SystemMaintenance implements ShouldQueue
nlog('Starting System Maintenance'); nlog('Starting System Maintenance');
if (Ninja::isHosted()) {
return;
}
$delete_pdf_days = config('ninja.maintenance.delete_pdfs'); $delete_pdf_days = config('ninja.maintenance.delete_pdfs');
nlog("Number of days to keep PDFs {$delete_pdf_days}"); nlog("Number of days to keep PDFs {$delete_pdf_days}");

View File

@ -42,39 +42,61 @@ 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); 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);
});
$this->checkForExpiredQuotes(); }
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 checkForExpiredQuotes()
{ {
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 queueExpiredQuoteNotification(Quote $quote) private function queueExpiredQuoteNotification(Quote $quote)

View File

@ -44,92 +44,124 @@ class ReminderJob implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() :void
{ {
if (! config('ninja.db.multi_db_enabled')) {
$this->processReminders();
} else {
//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();
}
}
}
private function processReminders()
{
nlog('Sending invoice reminders '.now()->format('Y-m-d h:i:s'));
set_time_limit(0); set_time_limit(0);
Invoice::query() if (! config('ninja.db.multi_db_enabled'))
->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) {
if ($invoice->isPayable()) {
//Attempts to prevent duplicates from sending nlog("Sending invoice reminders on ".now()->format('Y-m-d h:i:s'));
if($invoice->reminder_last_sent && Carbon::parse($invoice->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())){
nlog("caught a duplicate reminder for invoice {$invoice->number}"); Invoice::query()
return; ->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')->chunk(50, function ($invoices) {
foreach($invoices as $invoice)
{
$this->sendReminderForInvoice($invoice);
} }
$reminder_template = $invoice->calculateTemplate('invoice'); sleep(2);
nlog("reminder template = {$reminder_template}");
$invoice = $this->calcLateFee($invoice, $reminder_template);
$invoice->service()->touchReminder($reminder_template)->save();
$invoice->service()->touchPdf(true);
//20-04-2022 fixes for endless reminders - generic template naming was wrong });
$enabled_reminder = 'enable_'.$reminder_template;
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';
}
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { } else {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); //multiDB environment, need to
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
}); foreach (MultiDB::$dbs as $db)
{
if ($invoice->invitations->count() > 0) { MultiDB::setDB($db);
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
} nlog("Sending invoice reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
}
$invoice->service()->setReminder()->save(); Invoice::query()
} else { ->where('is_deleted', 0)
$invoice->next_send_date = null; ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
$invoice->save(); ->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')->chunk(50, function ($invoices) {
// if ($invoice->refresh() && $invoice->isPayable()) {
foreach($invoices as $invoice)
{
$this->sendReminderForInvoice($invoice);
}
sleep(2);
});
}
}
}
private function sendReminderForInvoice($invoice) {
if ($invoice->isPayable()) {
//Attempts to prevent duplicates from sending
if($invoice->reminder_last_sent && Carbon::parse($invoice->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())){
nlog("caught a duplicate reminder for invoice {$invoice->number}");
return;
}
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
//20-04-2022 fixes for endless reminders - generic template naming was wrong
$enabled_reminder = 'enable_'.$reminder_template;
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';
}
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template)->delay(now()->addSeconds(3));
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
} }
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
}
});
} }
/** /**
@ -220,16 +252,18 @@ class ReminderJob implements ShouldQueue
$invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance); $invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance);
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}"); $invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
$transaction = [ // $transaction = [
'invoice' => $invoice->transaction_event(), // 'invoice' => $invoice->transaction_event(),
'payment' => [], // 'payment' => [],
'client' => $invoice->client->transaction_event(), // 'client' => $invoice->client->transaction_event(),
'credit' => [], // 'credit' => [],
'metadata' => ['setLateFee'], // 'metadata' => ['setLateFee'],
]; // ];
// TransactionLog::dispatch(TransactionEvent::CLIENT_STATUS, $transaction, $invoice->company->db); // TransactionLog::dispatch(TransactionEvent::CLIENT_STATUS, $transaction, $invoice->company->db);
$invoice->service()->touchPdf(true);
return $invoice; return $invoice;
} }
} }

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,20 +44,47 @@ 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'));
$client = new Client(); if (config('ninja.db.multi_db_enabled')) {
$response = $client->get($cc_endpoint); foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$client = new Client();
$response = $client->get($cc_endpoint);
$currency_api = json_decode($response->getBody()); $currency_api = json_decode($response->getBody());
/* Update all currencies */ /* Update all currencies */
Currency::all()->each(function ($currency) use ($currency_api) { Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code}; $currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save(); $currency->save();
}); });
/* Rebuild the cache */ /* Rebuild the cache */
$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

@ -37,13 +37,14 @@ class VersionCheck implements ShouldQueue
{ {
$version_file = trim(@file_get_contents(config('ninja.version_url'))); $version_file = trim(@file_get_contents(config('ninja.version_url')));
nlog("latest version = {$version_file}");
if (Ninja::isSelfHost() && $version_file) { if (Ninja::isSelfHost() && $version_file) {
Account::whereNotNull('id')->update(['latest_version' => $version_file]); Account::whereNotNull('id')->update(['latest_version' => $version_file]);
} }
if (Ninja::isSelfHost()) { if (Ninja::isSelfHost()) {
nlog("latest version = {$version_file}");
$account = Account::first(); $account = Account::first();
if (! $account) { if (! $account) {

View File

@ -29,8 +29,6 @@ class InvoiceFailedEmailNotification
use UserNotifies, Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use UserNotifies, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $delay = 10;
public function __construct() public function __construct()
{ {
} }
@ -48,7 +46,7 @@ class InvoiceFailedEmailNotification
$first_notification_sent = true; $first_notification_sent = true;
$invoice = $event->invitation->invoice; $invoice = $event->invitation->invoice;
$invoice->update(['last_sent_date' => now()]); // $invoice->update(['last_sent_date' => now()]);
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new EntityFailedSendObject($event->invitation, 'invoice', $event->template, $event->message))->build()); $nmo->mailable = new NinjaMailer((new EntityFailedSendObject($event->invitation, 'invoice', $event->template, $event->message))->build());

View File

@ -331,7 +331,7 @@ class ACH
$data = [ $data = [
'gateway_type_id' => $cgt->gateway_type_id, 'gateway_type_id' => $cgt->gateway_type_id,
'payment_type' => PaymentType::ACH, 'payment_type' => PaymentType::ACH,
'transaction_reference' => $response->charges->data[0]->id, 'transaction_reference' => isset($response->latest_charge) ? $response->latest_charge : $response->charges->data[0]->id,
'amount' => $amount, 'amount' => $amount,
]; ];

View File

@ -135,7 +135,7 @@ class BrowserPay implements MethodInterface
'payment_method' => $gateway_response->payment_method, 'payment_method' => $gateway_response->payment_method,
'payment_type' => PaymentType::parseCardType(strtolower($payment_method->card->brand)), 'payment_type' => PaymentType::parseCardType(strtolower($payment_method->card->brand)),
'amount' => $this->stripe->convertFromStripeAmount($gateway_response->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), 'amount' => $this->stripe->convertFromStripeAmount($gateway_response->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => optional($payment_intent->charges->data[0])->id, 'transaction_reference' => isset($payment_intent->latest_charge) ? $payment_intent->latest_charge : $payment_intent->charges->data[0]->id,
'gateway_type_id' => GatewayType::APPLE_PAY, 'gateway_type_id' => GatewayType::APPLE_PAY,
]; ];

View File

@ -141,20 +141,27 @@ class Charge
$payment_method_type = PaymentType::SEPA; $payment_method_type = PaymentType::SEPA;
$status = Payment::STATUS_PENDING; $status = Payment::STATUS_PENDING;
} else { } else {
$payment_method_type = $response->charges->data[0]->payment_method_details->card->brand;
if(isset($response->latest_charge)) {
$charge = \Stripe\Charge::retrieve($response->latest_charge, $this->stripe->stripe_connect_auth);
$payment_method_type = $charge->payment_method_details->card->brand;
}
elseif(isset($response->charges->data[0]->payment_method_details->card->brand))
$payment_method_type = $response->charges->data[0]->payment_method_details->card->brand;
else
$payment_method_type = 'visa';
$status = Payment::STATUS_COMPLETED; $status = Payment::STATUS_COMPLETED;
} }
if($response?->status == 'processing'){ if(!in_array($response?->status, ['succeeded', 'processing'])){
//allows us to jump over the next stage - used for SEPA
}elseif($response?->status != 'succeeded'){
$this->stripe->processInternallyFailedPayment($this->stripe, new \Exception('Auto billing failed.',400)); $this->stripe->processInternallyFailedPayment($this->stripe, new \Exception('Auto billing failed.',400));
} }
$data = [ $data = [
'gateway_type_id' => $cgt->gateway_type_id, 'gateway_type_id' => $cgt->gateway_type_id,
'payment_type' => $this->transformPaymentTypeToConstant($payment_method_type), 'payment_type' => $this->transformPaymentTypeToConstant($payment_method_type),
'transaction_reference' => $response->charges->data[0]->id, 'transaction_reference' => isset($response->latest_charge) ? $response->latest_charge : $response->charges->data[0]->id,
'amount' => $amount, 'amount' => $amount,
]; ];
@ -162,6 +169,7 @@ class Charge
$payment->meta = $cgt->meta; $payment->meta = $cgt->meta;
$payment->save(); $payment->save();
$payment_hash->data = array_merge((array) $payment_hash->data, ['payment_intent' => $response, 'amount_with_fee' => $amount]);
$payment_hash->payment_id = $payment->id; $payment_hash->payment_id = $payment->id;
$payment_hash->save(); $payment_hash->save();

View File

@ -26,6 +26,7 @@ use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\QueryException;
use Stripe\Customer; use Stripe\Customer;
use Stripe\PaymentMethod; use Stripe\PaymentMethod;
@ -37,6 +38,8 @@ class ImportCustomers
/** @var StripePaymentDriver */ /** @var StripePaymentDriver */
public $stripe; public $stripe;
private bool $completed = true;
public $update_payment_methods; public $update_payment_methods;
public function __construct(StripePaymentDriver $stripe) public function __construct(StripePaymentDriver $stripe)
@ -64,9 +67,14 @@ class ImportCustomers
} }
//handle //handle
if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data))) // if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
$starting_after = end($customers->data)['id']; // $starting_after = end($customers->data)['id'];
else // else
// break;
$starting_after = isset(end($customers->data)['id']) ? end($customers->data)['id'] : false;
if(!$starting_after)
break; break;
} while ($customers->has_more); } while ($customers->has_more);
@ -132,10 +140,30 @@ class ImportCustomers
$client->name = $customer->name ? $customer->name : $customer->email; $client->name = $customer->name ? $customer->name : $customer->email;
if (! isset($client->number) || empty($client->number)) { if (! isset($client->number) || empty($client->number)) {
$client->number = $this->getNextClientNumber($client);
}
$client->save(); $x = 1;
do {
try {
$client->number = $this->getNextClientNumber($client);
$client->saveQuietly();
$this->completed = false;
} catch (QueryException $e) {
$x++;
if ($x > 10) {
$this->completed = false;
}
}
} while ($this->completed);
}
else{
$client->save();
}
$contact = ClientContactFactory::create($client->company_id, $client->user_id); $contact = ClientContactFactory::create($client->company_id, $client->user_id);
$contact->client_id = $client->id; $contact->client_id = $client->id;

View File

@ -138,6 +138,9 @@ class PaymentIntentWebhook implements ShouldQueue
$hash = isset($charge['metadata']['payment_hash']) ? $charge['metadata']['payment_hash'] : false; $hash = isset($charge['metadata']['payment_hash']) ? $charge['metadata']['payment_hash'] : false;
if(!$hash)
return;
$payment_hash = PaymentHash::where('hash', $hash)->first(); $payment_hash = PaymentHash::where('hash', $hash)->first();
if(!$payment_hash) if(!$payment_hash)
@ -264,6 +267,39 @@ class PaymentIntentWebhook implements ShouldQueue
} }
} }
// private function updateSepaPayment($payment_hash, $client, $meta)
// {
// $company_gateway = CompanyGateway::find($this->company_gateway_id);
// $payment_method_type = GatewayType::SEPA;
// $driver = $company_gateway->driver($client)->init()->setPaymentMethod($payment_method_type);
// $payment_hash->data = array_merge((array) $payment_hash->data, $this->stripe_request);
// $payment_hash->save();
// $driver->setPaymentHash($payment_hash);
// $data = [
// 'payment_method' => $payment_hash->data->object->payment_method,
// 'payment_type' => PaymentType::parseCardType(strtolower($meta['card_details'])) ?: PaymentType::CREDIT_CARD_OTHER,
// 'amount' => $payment_hash->data->amount_with_fee,
// 'transaction_reference' => $meta['transaction_reference'],
// 'gateway_type_id' => GatewayType::CREDIT_CARD,
// ];
// $payment = $driver->createPayment($data, Payment::STATUS_COMPLETED);
// SystemLogger::dispatch(
// ['response' => $this->stripe_request, 'data' => $data],
// SystemLog::CATEGORY_GATEWAY_RESPONSE,
// SystemLog::EVENT_GATEWAY_SUCCESS,
// SystemLog::TYPE_STRIPE,
// $client,
// $client->company,
// );
// }
private function updateCreditCardPayment($payment_hash, $client, $meta) private function updateCreditCardPayment($payment_hash, $client, $meta)
{ {

View File

@ -84,7 +84,7 @@ class SEPA
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all()); $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all());
$this->stripe->payment_hash->save(); $this->stripe->payment_hash->save();
if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status === 'succeeded')) { if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status == 'succeeded')) {
if ($request->store_card) { if ($request->store_card) {
$this->storePaymentMethod($gateway_response); $this->storePaymentMethod($gateway_response);
} }

View File

@ -660,14 +660,22 @@ class StripePaymentDriver extends BaseDriver
], $this->stripe_connect_auth); ], $this->stripe_connect_auth);
if ($charge->captured) { if ($charge->captured) {
$payment = Payment::query()
->where('transaction_reference', $transaction['payment_intent']) $payment = false;
->where('company_id', $request->getCompany()->id)
->where(function ($query) use ($transaction) { if(isset($transaction['payment_intent']))
$query->where('transaction_reference', $transaction['payment_intent']) {
->orWhere('transaction_reference', $transaction['id']); $payment = Payment::query()
}) ->where('transaction_reference', $transaction['payment_intent'])
->first(); ->where('company_id', $request->getCompany()->id)
->first();
}
elseif(isset($transaction['id'])) {
$payment = Payment::query()
->where('transaction_reference', $transaction['id'])
->where('company_id', $request->getCompany()->id)
->first();
}
if ($payment) { if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;

View File

@ -31,32 +31,16 @@ use Illuminate\Support\Facades\Cache;
class BankMatchingService implements ShouldQueue class BankMatchingService implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, GeneratesCounter; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private Company $company; public function __construct(protected int $company_id, private string $db){}
private $invoices; public function handle() :void
public $deleteWhenMissingModels = true;
public function __construct(private int $company_id, private string $db){}
public function handle()
{ {
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id); BankTransaction::where('company_id', $this->company_id)
$this->matchTransactions();
}
private function matchTransactions()
{
BankTransaction::where('company_id', $this->company->id)
->where('status_id', BankTransaction::STATUS_UNMATCHED) ->where('status_id', BankTransaction::STATUS_UNMATCHED)
->cursor() ->cursor()
->each(function ($bt){ ->each(function ($bt){
@ -64,11 +48,11 @@ class BankMatchingService implements ShouldQueue
(new BankService($bt))->processRules(); (new BankService($bt))->processRules();
}); });
} }
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->company_id)]; return [new WithoutOverlapping("bank_match_rate:{$this->company_id}")];
} }
} }

View File

@ -33,8 +33,6 @@ class ClientService
try { try {
\DB::connection(config('database.default'))->transaction(function () use($amount) { \DB::connection(config('database.default'))->transaction(function () use($amount) {
nlog("inside transaction - updating balance by {$amount}");
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $amount; $this->client->balance += $amount;
$this->client->save(); $this->client->save();

View File

@ -45,7 +45,7 @@ class SendEmail
$this->credit->invitations->each(function ($invitation) { $this->credit->invitations->each(function ($invitation) {
if (! $invitation->contact->trashed() && $invitation->contact->email) { if (! $invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template); EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template)->delay(2);
} }
}); });

View File

@ -112,8 +112,7 @@ class AddGatewayFee extends AbstractService
$this->invoice $this->invoice
->client ->client
->service() ->service()
->updateBalance($adjustment) ->updateBalance($adjustment);
->save();
$this->invoice $this->invoice
->ledger() ->ledger()

View File

@ -36,7 +36,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger); $this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300)); ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this; return $this;
} }
@ -52,7 +52,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger); $this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300)); ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this; return $this;
} }
@ -68,7 +68,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger); $this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300)); ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this; return $this;
} }

View File

@ -37,7 +37,7 @@ class SendEmail
$contact = $this->payment->client->contacts()->first(); $contact = $this->payment->client->contacts()->first();
if ($contact?->email) if ($contact?->email)
EmailPayment::dispatch($this->payment, $this->payment->company, $contact); EmailPayment::dispatch($this->payment, $this->payment->company, $contact)->delay(now()->addSeconds(3));
} }
} }

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();
$invoice_start_date = Carbon::parse($outstanding_invoice->date); if($outstanding_invoice)
$refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period); {
$invoice_start_date = Carbon::parse($outstanding_invoice->date);
$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

@ -70,7 +70,7 @@ trait Inviteable
$qr = $writer->writeString($this->getPaymentLink(), 'utf-8'); $qr = $writer->writeString($this->getPaymentLink(), 'utf-8');
return "<svg viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'> return "<svg class='pqrcode' viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'>
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>"; <rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>";
} }

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.42', 'app_version' => '5.5.43',
'app_tag' => '5.5.42', 'app_tag' => '5.5.43',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),
@ -211,5 +211,4 @@ return [
'dev_mode' => env("YODLEE_DEV_MODE", false), 'dev_mode' => env("YODLEE_DEV_MODE", false),
'config_name' => env("YODLEE_CONFIG_NAME", false), 'config_name' => env("YODLEE_CONFIG_NAME", false),
], ],
'dbs' => ['db-ninja-01','db-ninja-02'] ];
];

View File

@ -4,9 +4,12 @@ use App\Models\Currency;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use App\Utils\Traits\AppSetup;
return new class extends Migration return new class extends Migration
{ {
use AppSetup;
/** /**
* Run the migrations. * Run the migrations.
* *
@ -65,6 +68,9 @@ return new class extends Migration
\Illuminate\Support\Facades\Artisan::call('ninja:design-update'); \Illuminate\Support\Facades\Artisan::call('ninja:design-update');
$this->buildCache(true);
} }
/** /**

View File

@ -296,12 +296,12 @@ const RESOURCES = {
"canvaskit/profiling/canvaskit.wasm": "95a45378b69e77af5ed2bc72b2209b94", "canvaskit/profiling/canvaskit.wasm": "95a45378b69e77af5ed2bc72b2209b94",
"canvaskit/canvaskit.js": "2bc454a691c631b07a9307ac4ca47797", "canvaskit/canvaskit.js": "2bc454a691c631b07a9307ac4ca47797",
"canvaskit/canvaskit.wasm": "bf50631470eb967688cca13ee181af62", "canvaskit/canvaskit.wasm": "bf50631470eb967688cca13ee181af62",
"/": "686443a10b66af2e91462a76c539a39a", "/": "83aac01abae3eb27a43bdb1f55895304",
"version.json": "eaa34b1fc12792d815c0443fbfbc076b", "version.json": "04dda2a311adc1947afc5c1055f19e16",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40", "manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "301d099d604c2a0ce586552202f8d3ed" "main.dart.js": "91d67096ddd71f22b9cae01c774205f2"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

60975
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

60413
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.100","build_number":"100","package_name":"invoiceninja_flutter"} {"app_name":"invoiceninja_flutter","version":"5.0.102","build_number":"102","package_name":"invoiceninja_flutter"}

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>