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\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
@ -53,7 +54,7 @@ use Illuminate\Support\Str;
class DemoMode extends Command
{
use MakesHash, GeneratesCounter;
use MakesHash, GeneratesCounter, AppSetup;
protected $signature = 'ninja:demo-mode';
@ -83,34 +84,14 @@ class DemoMode extends Command
$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');
Artisan::call('migrate:fresh --force');
$this->info('Seeding');
Artisan::call('db:seed --force');
$this->buildCache(true);
$this->info('Seeding Random Data');
$this->createSmallAccount();

View File

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

View File

@ -48,46 +48,46 @@ class Kernel extends ConsoleKernel
$schedule->job(new VersionCheck)->daily();
/* 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 */
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping();
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* 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 */
$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 */
$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 */
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping();
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping();
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* 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 */
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping();
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
/* 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 */
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
/* 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 */
$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 */
$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()) {
@ -105,11 +105,11 @@ class Kernel extends ConsoleKernel
//not used @deprecate
// $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
* @return Builder
*/
public function sort(string $sort) : Builder
public function sort(string $sort = '') : Builder
{
$sort_col = explode('|', $sort);

View File

@ -539,7 +539,7 @@ class BaseController extends Controller
$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()) {
$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)));
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => [],
'client' => $invoice->client->transaction_event(),
'credit' => [],
'metadata' => [],
];
// $transaction = [
// 'invoice' => $invoice->transaction_event(),
// 'payment' => [],
// 'client' => $invoice->client->transaction_event(),
// 'credit' => [],
// 'metadata' => [],
// ];
// TransactionLog::dispatch(TransactionEvent::INVOICE_UPDATED, $transaction, $invoice->company->db);

View File

@ -44,7 +44,7 @@ class StoreBankIntegrationRequest extends Request
{
$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'];
$this->replace($input);

View File

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

View File

@ -32,8 +32,6 @@ class GenericReportRequest extends Request
return [
'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',
'start_date' => 'bail|required_if:date_range,custom|nullable|date',
'report_keys' => 'present|array',
@ -57,6 +55,12 @@ class GenericReportRequest extends Request
$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);
}
}

View File

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

View File

@ -24,7 +24,7 @@ class GenerateSmsRequest extends Request
*/
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'))
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';
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\Storage;
use function Amp\call;
class CompanyExport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
@ -531,6 +533,15 @@ class CompanyExport implements ShouldQueue
$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 = new \ZipArchive();

View File

@ -62,8 +62,15 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count().' partial invoices to auto bill');
$auto_bill_partial_invoices->cursor()->each(function ($invoice) {
AutoBill::dispatch($invoice->id, false);
$auto_bill_partial_invoices->chunk(100, function ($invoices) {
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
});
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
@ -79,8 +86,15 @@ class AutoBillCron
nlog($auto_bill_invoices->count().' full invoices to auto bill');
$auto_bill_invoices->cursor()->each(function ($invoice) {
AutoBill::dispatch($invoice->id, false);
$auto_bill_invoices->chunk(100, function ($invoices) {
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
});
} else {
//multiDB environment, need to
@ -100,8 +114,14 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count()." partial invoices to auto bill db = {$db}");
$auto_bill_partial_invoices->cursor()->each(function ($invoice) use ($db) {
AutoBill::dispatch($invoice->id, $db);
$auto_bill_partial_invoices->chunk(100, function ($invoices) use($db){
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, $db);
}
sleep(2);
});
$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}");
$auto_bill_invoices->cursor()->each(function ($invoice) use ($db) {
nlog($this->counter);
AutoBill::dispatch($invoice->id, $db);
$this->counter++;
$auto_bill_invoices->chunk(100, function ($invoices) use($db){
foreach($invoices as $invoice)
{
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'));
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 {
//multiDB environment, need to
foreach (MultiDB::$dbs as $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()
{
$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);
}
});
//extracting this back to the if/else block to test duplicate crons
}
private function generateExpense(RecurringExpense $recurring_expense)

View File

@ -41,44 +41,62 @@ class SubscriptionCron
nlog('Subscription Cron');
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 {
//multiDB environment, need to
foreach (MultiDB::$dbs as $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
*/
public function handle()
public function handle() :void
{
/* Don't fire emails if the company is disabled */
if ($this->company->is_disabled) {
return true;
return;
}
/* Set DB */

View File

@ -44,38 +44,30 @@ class BankTransactionSync implements ShouldQueue
*/
public function handle()
{
// if (! Ninja::isHosted()) {
// return;
// }
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
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()
{
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 {
//multiDB environment, need to
foreach (MultiDB::$dbs as $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
*/
public function handle()
public function handle() :void
{
LightLogs::create(new QueueSizeAnalytic(Queue::size()))
->send();

View File

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

View File

@ -42,39 +42,61 @@ class QuoteCheckExpired implements ShouldQueue
*/
public function handle()
{
if (! config('ninja.db.multi_db_enabled'))
return $this->checkForExpiredQuotes();
foreach (MultiDB::$dbs as $db) {
if (! config('ninja.db.multi_db_enabled')){
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()
{
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)

View File

@ -44,92 +44,124 @@ class ReminderJob implements ShouldQueue
*
* @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);
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) {
if ($invoice->isPayable()) {
if (! config('ninja.db.multi_db_enabled'))
{
//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;
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')->chunk(50, function ($invoices) {
foreach($invoices as $invoice)
{
$this->sendReminderForInvoice($invoice);
}
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice = $this->calcLateFee($invoice, $reminder_template);
$invoice->service()->touchReminder($reminder_template)->save();
$invoice->service()->touchPdf(true);
sleep(2);
//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);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db)
{
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();
MultiDB::setDB($db);
nlog("Sending invoice reminders on db {$db} ".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')->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->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => [],
'client' => $invoice->client->transaction_event(),
'credit' => [],
'metadata' => ['setLateFee'],
];
// $transaction = [
// 'invoice' => $invoice->transaction_event(),
// 'payment' => [],
// 'client' => $invoice->client->transaction_event(),
// 'credit' => [],
// 'metadata' => ['setLateFee'],
// ];
// TransactionLog::dispatch(TransactionEvent::CLIENT_STATUS, $transaction, $invoice->company->db);
$invoice->service()->touchPdf(true);
return $invoice;
}
}

View File

@ -34,19 +34,7 @@ class UpdateExchangeRates implements ShouldQueue
*
* @return void
*/
public function handle()
{
if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->updateCurrencies();
}
} else {
$this->updateCurrencies();
}
}
private function updateCurrencies()
public function handle() :void
{
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'));
$client = new Client();
$response = $client->get($cc_endpoint);
if (config('ninja.db.multi_db_enabled')) {
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 */
Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save();
});
/* 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();
/* Rebuild the cache */
$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')));
nlog("latest version = {$version_file}");
if (Ninja::isSelfHost() && $version_file) {
Account::whereNotNull('id')->update(['latest_version' => $version_file]);
}
if (Ninja::isSelfHost()) {
nlog("latest version = {$version_file}");
$account = Account::first();
if (! $account) {

View File

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

View File

@ -331,7 +331,7 @@ class ACH
$data = [
'gateway_type_id' => $cgt->gateway_type_id,
'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,
];

View File

@ -135,7 +135,7 @@ class BrowserPay implements MethodInterface
'payment_method' => $gateway_response->payment_method,
'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()),
'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,
];

View File

@ -141,20 +141,27 @@ class Charge
$payment_method_type = PaymentType::SEPA;
$status = Payment::STATUS_PENDING;
} 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;
}
if($response?->status == 'processing'){
//allows us to jump over the next stage - used for SEPA
}elseif($response?->status != 'succeeded'){
if(!in_array($response?->status, ['succeeded', 'processing'])){
$this->stripe->processInternallyFailedPayment($this->stripe, new \Exception('Auto billing failed.',400));
}
$data = [
'gateway_type_id' => $cgt->gateway_type_id,
'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,
];
@ -162,6 +169,7 @@ class Charge
$payment->meta = $cgt->meta;
$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->save();

View File

@ -26,6 +26,7 @@ use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\QueryException;
use Stripe\Customer;
use Stripe\PaymentMethod;
@ -37,6 +38,8 @@ class ImportCustomers
/** @var StripePaymentDriver */
public $stripe;
private bool $completed = true;
public $update_payment_methods;
public function __construct(StripePaymentDriver $stripe)
@ -64,9 +67,14 @@ class ImportCustomers
}
//handle
if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
$starting_after = end($customers->data)['id'];
else
// if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
// $starting_after = end($customers->data)['id'];
// else
// break;
$starting_after = isset(end($customers->data)['id']) ? end($customers->data)['id'] : false;
if(!$starting_after)
break;
} while ($customers->has_more);
@ -132,10 +140,30 @@ class ImportCustomers
$client->name = $customer->name ? $customer->name : $customer->email;
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->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;
if(!$hash)
return;
$payment_hash = PaymentHash::where('hash', $hash)->first();
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)
{

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->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) {
$this->storePaymentMethod($gateway_response);
}

View File

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

View File

@ -31,32 +31,16 @@ use Illuminate\Support\Facades\Cache;
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 $deleteWhenMissingModels = true;
public function __construct(private int $company_id, private string $db){}
public function handle()
public function handle() :void
{
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
$this->matchTransactions();
}
private function matchTransactions()
{
BankTransaction::where('company_id', $this->company->id)
BankTransaction::where('company_id', $this->company_id)
->where('status_id', BankTransaction::STATUS_UNMATCHED)
->cursor()
->each(function ($bt){
@ -64,11 +48,11 @@ class BankMatchingService implements ShouldQueue
(new BankService($bt))->processRules();
});
}
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 {
\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->balance += $amount;
$this->client->save();

View File

@ -45,7 +45,7 @@ class SendEmail
$this->credit->invitations->each(function ($invitation) {
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
->client
->service()
->updateBalance($adjustment)
->save();
->updateBalance($adjustment);
$this->invoice
->ledger()

View File

@ -36,7 +36,7 @@ class LedgerService
$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;
}
@ -52,7 +52,7 @@ class LedgerService
$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;
}
@ -68,7 +68,7 @@ class LedgerService
$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;
}

View File

@ -37,7 +37,7 @@ class SendEmail
$contact = $this->payment->client->contacts()->first();
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)
{
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) {
return ["message" => "Success", "status_code" => 200];
@ -901,6 +901,8 @@ class SubscriptionService
*/
public function handleCancellation(RecurringInvoice $recurring_invoice)
{
$invoice_start_date = false;
$refund_end_date = false;
//only refund if they are in the refund window.
$outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id)
@ -909,8 +911,11 @@ class SubscriptionService
->orderBy('id', 'desc')
->first();
$invoice_start_date = Carbon::parse($outstanding_invoice->date);
$refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period);
if($outstanding_invoice)
{
$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 */
$recurring_invoice->service()->stop()->save();
@ -918,7 +923,7 @@ class SubscriptionService
$recurring_invoice_repo->archive($recurring_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())

View File

@ -70,7 +70,7 @@ trait Inviteable
$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>";
}

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.42',
'app_tag' => '5.5.42',
'app_version' => '5.5.43',
'app_tag' => '5.5.43',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -211,5 +211,4 @@ return [
'dev_mode' => env("YODLEE_DEV_MODE", 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\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Utils\Traits\AppSetup;
return new class extends Migration
{
use AppSetup;
/**
* Run the migrations.
*
@ -65,6 +68,9 @@ return new class extends Migration
\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/canvaskit.js": "2bc454a691c631b07a9307ac4ca47797",
"canvaskit/canvaskit.wasm": "bf50631470eb967688cca13ee181af62",
"/": "686443a10b66af2e91462a76c539a39a",
"version.json": "eaa34b1fc12792d815c0443fbfbc076b",
"/": "83aac01abae3eb27a43bdb1f55895304",
"version.json": "04dda2a311adc1947afc5c1055f19e16",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"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

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">
@include('portal.ninja2020.quotes.includes.actions', ['quote' => $quote])
</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="px-4 py-5 sm:p-6">
@ -57,7 +57,7 @@
</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="px-4 py-5 sm:p-6">

View File

@ -94,7 +94,7 @@
</div>
<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 }">
<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>
@include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
</div>