Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2022-05-18 09:43:05 +10:00
commit 11ab8462a6
65 changed files with 813624 additions and 794822 deletions

View File

@ -46,7 +46,7 @@ TRUSTED_PROXIES=
NINJA_ENVIRONMENT=selfhost
#options - snappdf / phantom / hosted_ninja
PDF_GENERATOR=phantom
PDF_GENERATOR=hosted_ninja
PHANTOMJS_KEY='a-demo-key-with-low-quota-per-ip-address'
PHANTOMJS_SECRET=secret

View File

@ -1 +1 @@
5.3.88
5.3.89

View File

@ -607,7 +607,7 @@ class CheckData extends Command
invoices ON
clients.id=invoices.client_id
WHERE invoices.is_deleted = false
AND invoices.status_id IN (2,3,4)
AND invoices.status_id IN (2,3)
GROUP BY clients.id
HAVING invoice_balance != clients.balance
ORDER BY clients.id;
@ -663,7 +663,7 @@ class CheckData extends Command
ON invoices.client_id = clients.id
WHERE invoices.is_deleted = 0
AND clients.is_deleted = 0
AND invoices.status_id IN (2,3,4)
AND invoices.status_id IN (2,3)
GROUP BY clients.id
HAVING(invoices_balance != clients.balance)
ORDER BY clients.id;
@ -683,7 +683,7 @@ class CheckData extends Command
{
$client = Client::withTrashed()->find($_client->id);
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3,4])->sum('balance');
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance');
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
@ -724,7 +724,7 @@ class CheckData extends Command
$this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3,4])->sum('balance');
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance');
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($ledger->balance, 4) != number_format($client->balance, 4)) {

View File

@ -18,6 +18,7 @@ use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Ledger\LedgerBalanceUpdate;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Ninja\QueueSize;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
@ -55,7 +56,8 @@ class Kernel extends ConsoleKernel
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping();
$schedule->job(new LedgerBalanceUpdate)->everyFiveMinutes()->withoutOverlapping();
// $schedule->job(new LedgerBalanceUpdate)->everyFiveMinutes()->withoutOverlapping();
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping();
$schedule->job(new CompanySizeCheck)->daily()->withoutOverlapping();
@ -87,9 +89,9 @@ class Kernel extends ConsoleKernel
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily('01:00')->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily('02:00')->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('01:05')->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:05')->withoutOverlapping();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping();

View File

@ -0,0 +1,79 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class QueueSize extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'ninja.queue_size';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = 'stub';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = 'stub';
/**
* The counter
* set to 1.
*
* @var string
*/
public $int_metric1 = 1;
/**
* Company Key
* @var string
*/
public $string_metric7 = '';
public function __construct($int_metric1) {
$this->int_metric1 = $int_metric1;
}
}

View File

@ -30,7 +30,8 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invitations']);
//preserve terms if they exist on Quotes
if(array_key_exists('terms', $quote_array) && strlen($quote_array['terms']) < 2)
//if(array_key_exists('terms', $quote_array) && strlen($quote_array['terms']) < 2)
if(!$quote->company->use_quote_terms_on_conversion)
unset($quote_array['terms']);
// unset($quote_array['public_notes']);
@ -38,7 +39,6 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['design_id']);
unset($quote_array['user']);
foreach ($quote_array as $key => $value) {
$invoice->{$key} = $value;
}

View File

@ -25,10 +25,12 @@ use App\Http\Requests\Invoice\EditInvoiceRequest;
use App\Http\Requests\Invoice\ShowInvoiceRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Invoice\UpdateInvoiceRequest;
use App\Http\Requests\Invoice\UpdateReminderRequest;
use App\Http\Requests\Invoice\UploadInvoiceRequest;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\Invoice\BulkInvoiceJob;
use App\Jobs\Invoice\StoreInvoice;
use App\Jobs\Invoice\UpdateReminders;
use App\Jobs\Invoice\ZipInvoices;
use App\Jobs\Ninja\TransactionLog;
use App\Jobs\Util\UnlinkFile;
@ -957,4 +959,12 @@ class InvoiceController extends BaseController
return $this->itemResponse($invoice->fresh());
}
public function update_reminders(UpdateReminderRequest $request)
{
UpdateReminders::dispatch(auth()->user()->company());
return response()->json(["message" => "Updating reminders"], 200);
}
}

View File

@ -34,6 +34,7 @@ class SubdomainController extends BaseController
'stage',
'html',
'lb',
'shopify',
];
public function __construct()

View File

@ -29,6 +29,7 @@ class WePayController extends BaseController
*/
public function signup(string $token)
{
return render('gateways.wepay.signup.finished');
$hash = Cache::get($token);

View File

@ -83,7 +83,7 @@ class InvoicesTable extends Component
return render('components.livewire.invoices-table', [
'invoices' => $query,
'gateway_available' => !empty(auth()->user()->client->service()->getPaymentMethods(0)),
'gateway_available' => !empty(auth()->user()->client->service()->getPaymentMethods(-1)),
]);
}
}

View File

@ -22,7 +22,7 @@ class CreatePaymentMethodRequest extends FormRequest
$available_methods = [];
collect($client->service()->getPaymentMethods(1))
collect($client->service()->getPaymentMethods(-1))
->filter(function ($method) use (&$available_methods) {
$available_methods[] = $method['gateway_type_id'];
});

View File

@ -0,0 +1,32 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
class UpdateReminderRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
public function rules()
{
return [];
}
}

View File

@ -104,7 +104,7 @@ class StorePaymentRequest extends Request
'credits.*.credit_id' => new ValidCreditsRules($this->all()),
'credits.*.amount' => ['required', new CreditsSumRule($this->all())],
'invoices' => new ValidPayableInvoicesRule(),
'number' => ['nullable', Rule::unique('payments')->where('company_id', auth()->user()->company()->id)],
'number' => ['nullable', 'bail', Rule::unique('payments')->where('company_id', auth()->user()->company()->id)],
];

View File

@ -14,6 +14,9 @@ namespace App\Http\Requests\Preview;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Models\Invoice;
use App\Models\Credit;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -30,15 +33,13 @@ class PreviewInvoiceRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->can('create', Invoice::class);
return auth()->user()->can('create', Invoice::class) || auth()->user()->can('create', Quote::class) || auth()->user()->can('create', RecurringInvoice::class) || auth()->user()->can('create', Credit::class);
}
public function rules()
{
$rules = [];
// $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id;
$rules['number'] = ['nullable'];
return $rules;

View File

@ -32,7 +32,8 @@ class GenericReportRequest extends Request
'end_date' => 'string|date',
'date_key' => 'string',
'date_range' => 'string',
'report_keys' => 'present|array'
'report_keys' => 'present|array',
'send_email' => 'bool',
];
}
}

View File

@ -33,7 +33,8 @@ class ProfitLossRequest extends Request
'is_income_billed' => 'required|bail|bool',
'is_expense_billed' => 'required|bail|bool',
'include_tax' => 'required|bail|bool',
'date_range' => 'required|bail|string'
'date_range' => 'required|bail|string',
'send_email' => 'bool',
];
}
}

View File

@ -53,6 +53,7 @@ class AutoBillCron
$auto_bill_partial_invoices = Invoice::whereDate('partial_due_date', '<=', now())
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('auto_bill_enabled', true)
->where('auto_bill_tries', '<', 3)
->where('balance', '>', 0)
->where('is_deleted', false)
->whereHas('company', function ($query) {
@ -70,6 +71,7 @@ class AutoBillCron
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('auto_bill_enabled', true)
->where('auto_bill_tries', '<', 3)
->where('balance', '>', 0)
->where('is_deleted', false)
->whereHas('company', function ($query) {
@ -94,6 +96,7 @@ class AutoBillCron
$auto_bill_partial_invoices = Invoice::whereDate('partial_due_date', '<=', now())
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('auto_bill_enabled', true)
->where('auto_bill_tries', '<', 3)
->where('balance', '>', 0)
->where('is_deleted', false)
->whereHas('company', function ($query) {
@ -111,6 +114,7 @@ class AutoBillCron
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('auto_bill_enabled', true)
->where('auto_bill_tries', '<', 3)
->where('balance', '>', 0)
->where('is_deleted', false)
->whereHas('company', function ($query) {

View File

@ -67,11 +67,23 @@ class RecurringInvoicesCron
nlog("Trying to send {$recurring_invoice->number}");
/* Special check if we should generate another invoice is the previous one is yet to be paid */
if($recurring_invoice->company->stop_on_unpaid_recurring && $recurring_invoice->invoices()->whereIn('status_id', [2,3])->where('is_deleted', 0)->where('balance', '>', 0)->exists()) {
nlog("Existing invoice exists, skipping");
return;
}
try{
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}
catch(\Exception $e){
nlog("Unable to sending recurring invoice {$recurring_invoice->id} ". $e->getMessage());
}
});
@ -103,6 +115,13 @@ class RecurringInvoicesCron
nlog("Trying to send {$recurring_invoice->number}");
if($recurring_invoice->company->stop_on_unpaid_recurring) {
if($recurring_invoice->invoices()->whereIn('status_id', [2,3])->where('is_deleted', 0)->where('balance', '>', 0)->exists())
return;
}
try{
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}

View File

@ -38,6 +38,8 @@ class SubscriptionCron
public function handle() : void
{
nlog("Subscription Cron");
if (! config('ninja.db.multi_db_enabled')) {
$this->loopSubscriptions();

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Invoice;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UpdateReminders implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Company $company;
public function __construct(Company $company)
{
$this->company = $company;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
MultiDB::setDb($this->company->db);
$this->company
->invoices()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->cursor()
->each(function ($invoice){
$invoice->service()->setReminder()->save();
});
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Ledger;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyLedger;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ClientLedgerBalanceUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $company;
public $client;
public function __construct(Company $company, Client $client)
{
$this->company = $company;
$this->client = $client;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle() :void
{
nlog("Updating company ledgers");
MultiDB::setDb($this->company->db);
CompanyLedger::where('balance', 0)->where('client_id', $this->client->id)->cursor()->each(function ($company_ledger){
if($company_ledger->balance > 0)
return;
$last_record = CompanyLedger::where('client_id', $company_ledger->client_id)
->where('company_id', $company_ledger->company_id)
->where('balance', '!=', 0)
->orderBy('id', 'DESC')
->first();
if(!$last_record)
return;
nlog("Updating Balance NOW");
$company_ledger->balance = $last_record->balance + $company_ledger->adjustment;
$company_ledger->save();
});
nlog("Finished checking company ledgers");
}
public function checkLedger()
{
}
}

View File

@ -24,6 +24,8 @@ class LedgerBalanceUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct()
{
}
@ -34,26 +36,31 @@ class LedgerBalanceUpdate implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle() :void
{
nlog("Updating company ledgers");
if (! config('ninja.db.multi_db_enabled')) {
$this->check();
$this->checkLedger();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->check();
$this->checkLedger();
}
}
nlog("Finished checking company ledgers");
}
public function check()
public function checkLedger()
{
CompanyLedger::where('balance', 0)->cursor()->each(function ($company_ledger){
nlog("Checking ledgers....");
CompanyLedger::where('balance', 0)->where('adjustment', '!=', 0)->cursor()->each(function ($company_ledger){
if($company_ledger->balance > 0)
return;
@ -67,6 +74,8 @@ class LedgerBalanceUpdate implements ShouldQueue
if(!$last_record)
return;
nlog("Updating Balance NOW");
$company_ledger->balance = $last_record->balance + $company_ledger->adjustment;
$company_ledger->save();

View File

@ -111,9 +111,10 @@ class PaymentFailedMailer implements ShouldQueue
//add client payment failures here.
//
if($contact = $this->client->contacts()->first() && $this->payment_hash)
if($this->client->contacts()->whereNotNull('email')->exists() && $this->payment_hash)
{
$contact = $this->client->contacts()->whereNotNull('email')->first();
$mail_obj = (new ClientPaymentFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build();
$nmo = new NinjaMailerObject;

View File

@ -0,0 +1,48 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Ninja;
use App\DataMapper\Analytics\QueueSize as QueueSizeAnalytic;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Queue;
use Turbo124\Beacon\Facades\LightLogs;
use Turbo124\Beacon\Jobs\Database\MySQL\DbStatus;
class QueueSize implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
LightLogs::create(new QueueSizeAnalytic(Queue::size()))
->queue();
}
}

View File

@ -36,7 +36,7 @@ class DiskCleanup implements ShouldQueue
*/
public function handle()
{
nlog("Cleaning Storage");
// Get all files in a directory
$files = Storage::allFiles(config('filesystems.default'), 'backups/');

View File

@ -153,10 +153,12 @@ class InvoiceEmailEngine extends BaseEmailEngine
$line_items = $this->invoice->line_items;
$expense_ids = [];
foreach($line_items as $item)
{
$expense_ids = [];
if(property_exists($item, 'expense_id'))
{
$expense_ids[] = $item->expense_id;
@ -177,6 +179,8 @@ class InvoiceEmailEngine extends BaseEmailEngine
});
}
$task_ids = [];
if(property_exists($item, 'task_id'))
{
$task_ids[] = $item->task_id;

View File

@ -432,7 +432,7 @@ class Client extends BaseModel implements HasLocalePreference
public function getCreditCardGateway() :?CompanyGateway
{
$pms = $this->service()->getPaymentMethods(0);
$pms = $this->service()->getPaymentMethods(-1);
foreach($pms as $pm)
{
@ -462,7 +462,7 @@ class Client extends BaseModel implements HasLocalePreference
//todo refactor this - it is only searching for existing tokens
public function getBankTransferGateway() :?CompanyGateway
{
$pms = $this->service()->getPaymentMethods(0);
$pms = $this->service()->getPaymentMethods(-1);
if($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){

View File

@ -100,6 +100,9 @@ class Company extends BaseModel
'client_registration_fields',
'convert_rate_to_client',
'markdown_email_enabled',
'stop_on_unpaid_recurring',
'use_quote_terms_on_conversion',
'show_production_description_dropdown',
];
protected $hidden = [

View File

@ -48,4 +48,9 @@ class CompanyLedger extends Model
{
return $this->morphTo();
}
public function client()
{
return $this->belongsTo(Client::class);
}
}

View File

@ -90,7 +90,7 @@ class Invoice extends BaseModel
'subscription_id',
'auto_bill_enabled',
'uses_inclusive_taxes',
'vendor_id',
'vendor_id'
];
protected $casts = [
@ -445,14 +445,14 @@ class Invoice extends BaseModel
$file_path = CreateEntityPdf::dispatchNow($invitation, config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
try{
$file_exists = Storage::disk('public')->exists($file_path);
}
catch(\Exception $e){
nlog($e->getMessage());
}
if($file_exists)
@ -553,10 +553,10 @@ class Invoice extends BaseModel
$invoice = $this->fresh();
return [
'invoice_id' => $invoice->id,
'invoice_amount' => $invoice->amount ?: 0,
'invoice_partial' => $invoice->partial ?: 0,
'invoice_balance' => $invoice->balance ?: 0,
'invoice_id' => $invoice->id,
'invoice_amount' => $invoice->amount ?: 0,
'invoice_partial' => $invoice->partial ?: 0,
'invoice_balance' => $invoice->balance ?: 0,
'invoice_paid_to_date' => $invoice->paid_to_date ?: 0,
'invoice_status' => $invoice->status_id ?: 1,
];

View File

@ -227,12 +227,21 @@ class RecurringInvoice extends BaseModel
if (!$this->next_send_date) {
return null;
}
nlog("frequency = $this->frequency_id");
nlog("frequency = $this->next_send_date");
$offset = $this->client->timezone_offset();
/* If this setting is enabled, the recurring invoice may be set in the past */
if($this->company->stop_on_unpaid_recurring) {
/* Lets set the next send date to now so we increment from today, rather than in the past*/
if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3)))
$this->next_send_date = now()->format('Y-m-d');
}
/*
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds

View File

@ -30,7 +30,7 @@ class ClientObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_CLIENT, $client, $client->company);
WebhookHandler::dispatch(Webhook::EVENT_CREATE_CLIENT, $client, $client->company)->delay(now()->addSeconds(2));
}
}
@ -47,7 +47,7 @@ class ClientObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_CLIENT, $client, $client->company);
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_CLIENT, $client, $client->company)->delay(now()->addSeconds(2));
}
}
@ -64,7 +64,7 @@ class ClientObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_DELETE_CLIENT, $client, $client->company);
WebhookHandler::dispatch(Webhook::EVENT_DELETE_CLIENT, $client, $client->company)->delay(now()->addSeconds(2));
}
}

View File

@ -30,7 +30,7 @@ class ExpenseObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_EXPENSE, $expense, $expense->company);
WebhookHandler::dispatch(Webhook::EVENT_CREATE_EXPENSE, $expense, $expense->company)->delay(now()->addSeconds(2));
}
}
@ -47,7 +47,7 @@ class ExpenseObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_EXPENSE, $expense, $expense->company);
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_EXPENSE, $expense, $expense->company)->delay(now()->addSeconds(2));
}
}
@ -64,7 +64,7 @@ class ExpenseObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_DELETE_EXPENSE, $expense, $expense->company);
WebhookHandler::dispatch(Webhook::EVENT_DELETE_EXPENSE, $expense, $expense->company)->delay(now()->addSeconds(2));
}
}

View File

@ -36,7 +36,7 @@ class InvoiceObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company, 'client')->delay(now()->addSeconds(2));
}
}
@ -55,7 +55,7 @@ class InvoiceObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company, 'client')->delay(now()->addSeconds(2));
}
@ -75,7 +75,7 @@ class InvoiceObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_DELETE_INVOICE, $invoice, $invoice->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_INVOICE, $invoice, $invoice->company, 'client')->delay(now()->addSeconds(2));
}
}

View File

@ -30,7 +30,7 @@ class PaymentObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_PAYMENT, $payment, $payment->company, 'invoices,client')->delay(5);
WebhookHandler::dispatch(Webhook::EVENT_CREATE_PAYMENT, $payment, $payment->company, 'invoices,client')->delay(now()->addSeconds(20));
}
}
@ -57,7 +57,7 @@ class PaymentObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_DELETE_PAYMENT, $payment, $payment->company, 'invoices,client')->delay(5);
WebhookHandler::dispatch(Webhook::EVENT_DELETE_PAYMENT, $payment, $payment->company, 'invoices,client')->delay(now()->addSeconds(20));
}
}

View File

@ -32,7 +32,7 @@ class ProjectObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_CREATE, $project, $project->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_CREATE, $project, $project->company, 'client')->delay(now()->addSeconds(2));
}
}
@ -50,7 +50,7 @@ class ProjectObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_UPDATE, $project, $project->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_UPDATE, $project, $project->company, 'client')->delay(now()->addSeconds(2));
}
}

View File

@ -33,7 +33,7 @@ class QuoteObserver
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_QUOTE, $quote, $quote->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_QUOTE, $quote, $quote->company, 'client')->delay(now()->addSeconds(2));
}
}
@ -53,7 +53,7 @@ class QuoteObserver
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company, 'client')->delay(now()->addSeconds(2));
}
}
@ -72,7 +72,7 @@ class QuoteObserver
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_QUOTE, $quote, $quote->company, 'client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_QUOTE, $quote, $quote->company, 'client')->delay(now()->addSeconds(2));
}
}

View File

@ -30,7 +30,7 @@ class TaskObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_TASK, $task, $task->company);
WebhookHandler::dispatch(Webhook::EVENT_CREATE_TASK, $task, $task->company)->delay(now()->addSeconds(2));
}
}
@ -47,7 +47,7 @@ class TaskObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_TASK, $task, $task->company);
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_TASK, $task, $task->company)->delay(now()->addSeconds(2));
}
}
@ -64,7 +64,7 @@ class TaskObserver
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_DELETE_TASK, $task, $task->company);
WebhookHandler::dispatch(Webhook::EVENT_DELETE_TASK, $task, $task->company)->delay(now()->addSeconds(2));
}
}

View File

@ -245,7 +245,7 @@ class GoCardlessPaymentDriver extends BaseDriver
sleep(1);
foreach ($request->events as $event) {
if ($event['action'] === 'confirmed') {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out') {
nlog("Searching for transaction reference");

View File

@ -568,7 +568,7 @@ class StripePaymentDriver extends BaseDriver
//payment_intent.succeeded - this will confirm or cancel the payment
if($request->type === 'payment_intent.succeeded'){
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(10);
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(10));
return response()->json([], 200);
}
@ -579,7 +579,7 @@ class StripePaymentDriver extends BaseDriver
if(array_key_exists('payment_intent', $transaction))
{
$payment = Payment::query()
->where('company_id', $request->getCompany()->id)
// ->where('company_id', $request->getCompany()->id)
->where(function ($query) use ($transaction) {
$query->where('transaction_reference', $transaction['payment_intent'])
->orWhere('transaction_reference', $transaction['id']);
@ -589,7 +589,7 @@ class StripePaymentDriver extends BaseDriver
else
{
$payment = Payment::query()
->where('company_id', $request->getCompany()->id)
// ->where('company_id', $request->getCompany()->id)
->where('transaction_reference', $transaction['id'])
->first();
}

View File

@ -17,6 +17,7 @@ use App\Models\Company;
use App\Utils\Traits\ClientGroupSettingsSaver;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Database\QueryException;
/**
* ClientRepository.
@ -26,6 +27,8 @@ class ClientRepository extends BaseRepository
use GeneratesCounter;
use SavesDocuments;
private bool $completed = true;
/**
* @var ClientContactRepository
*/
@ -76,8 +79,34 @@ class ClientRepository extends BaseRepository
if (!isset($client->number) || empty($client->number) || strlen($client->number) == 0) {
$client->number = $this->getNextClientNumber($client);
$client->save();
// $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);
}
if (empty($data['name'])) {

View File

@ -132,8 +132,7 @@ class PaymentMethod
private function getMethods()
{
// we should prefilter $gateway->driver($this)->gatewayTypes()
// and only include the enabled payment methods on the gateway
$this->payment_methods = [];
foreach ($this->gateways as $gateway) {
@ -148,13 +147,12 @@ class PaymentMethod
if ($this->validGatewayForAmount($gateway->fees_and_limits->{$type}, $this->amount) && $gateway->fees_and_limits->{$type}->is_enabled) {
// if($type == GatewayType::BANK_TRANSFER);
$this->payment_methods[] = [$gateway->id => $type];
}
} else {
//$this->payment_methods[] = [$gateway->id => $type];
}
}
}
@ -243,11 +241,11 @@ class PaymentMethod
return true;
}
if ((property_exists($fees_and_limits, 'min_limit')) && $fees_and_limits->min_limit !== null && $fees_and_limits->min_limit != -1 && $this->amount < $fees_and_limits->min_limit) {
if ((property_exists($fees_and_limits, 'min_limit')) && $fees_and_limits->min_limit !== null && $fees_and_limits->min_limit != -1 && ($this->amount < $fees_and_limits->min_limit && $this->amount != -1)) {
return false;
}
if ((property_exists($fees_and_limits, 'max_limit')) && $fees_and_limits->max_limit !== null && $fees_and_limits->max_limit != -1 && $this->amount > $fees_and_limits->max_limit) {
if ((property_exists($fees_and_limits, 'max_limit')) && $fees_and_limits->max_limit !== null && $fees_and_limits->max_limit != -1 && ($this->amount > $fees_and_limits->max_limit && $this->amount != -1)) {
return false;
}

View File

@ -45,8 +45,8 @@ class SendEmail
$this->credit->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// $email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}

View File

@ -38,7 +38,7 @@ class AutoBillInvoice extends AbstractService
public function __construct(Invoice $invoice, $db)
{
$this->invoice = $invoice;
$this->db = $db;
}
@ -52,7 +52,7 @@ class AutoBillInvoice extends AbstractService
$is_partial = false;
/* Is the invoice payable? */
if (! $this->invoice->isPayable())
if (! $this->invoice->isPayable())
return $this->invoice;
/* Mark the invoice as sent */
@ -114,25 +114,29 @@ class AutoBillInvoice extends AbstractService
]);
nlog("Payment hash created => {$payment_hash->id}");
$payment = false;
try{
$payment = $gateway_token->gateway
->driver($this->client)
->setPaymentHash($payment_hash)
->tokenBilling($gateway_token, $payment_hash);
}
catch(\Exception $e){
nlog("payment NOT captured for ". $this->invoice->number . " with error " . $e->getMessage());
// $this->invoice->service()->removeUnpaidGatewayFees();
}
try {
$payment = $gateway_token->gateway
->driver($this->client)
->setPaymentHash($payment_hash)
->tokenBilling($gateway_token, $payment_hash);
} catch (\Exception $e) {
$this->invoice->auto_bill_tries = $number_of_retries + 1;
if($payment){
info("Auto Bill payment captured for ".$this->invoice->number);
if ($this->invoice->auto_bill_tries == 3) {
$this->invoice->auto_bill_enabled = false;
$this->invoice->auto_bill_tries = 0; //reset the counter here in case auto billing is turned on again in the future.
$this->invoice->save();
}
nlog("payment NOT captured for " . $this->invoice->number . " with error " . $e->getMessage());
}
if ($payment) {
info("Auto Bill payment captured for " . $this->invoice->number);
}
// return $this->invoice->fresh();
}
/**

View File

@ -35,7 +35,7 @@ class UpdateReminder extends AbstractService
$this->settings = $this->invoice->client->getMergedSettings();
}
if (! $this->invoice->isPayable()) {
if (! $this->invoice->isPayable() || $this->invoice->status_id == Invoice::STATUS_DRAFT) {
$this->invoice->next_send_date = null;
$this->invoice->saveQuietly();
@ -55,7 +55,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder1)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 1 after due date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder1_sent) &&
@ -64,7 +64,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder1)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 1 before_due_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder1_sent) &&
@ -73,7 +73,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder1)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 1 after_due_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder2_sent) &&
@ -82,7 +82,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder2)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 2 after_invoice_date = ".$reminder_date->format('Y-m-d'));
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder2_sent) &&
@ -91,7 +91,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder2)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 2 before_due_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder2_sent) &&
@ -100,7 +100,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder2)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 2 after_due_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder3_sent) &&
@ -109,7 +109,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder3)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 3 after_invoice_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder3_sent) &&
@ -118,7 +118,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder3)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 3 before_due_date");
$date_collection->push($reminder_date);
}
if (is_null($this->invoice->reminder3_sent) &&
@ -127,7 +127,7 @@ class UpdateReminder extends AbstractService
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder3)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings reminder 3 after_due_date");
$date_collection->push($reminder_date);
}
if ($this->invoice->last_sent_date &&
@ -140,7 +140,7 @@ class UpdateReminder extends AbstractService
$reminder_date->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)))
$date_collection->push($reminder_date); nlog("settings endless reminder");
$date_collection->push($reminder_date);
}

View File

@ -12,6 +12,7 @@
namespace App\Services\Ledger;
use App\Factory\CompanyLedgerFactory;
use App\Jobs\Ledger\ClientLedgerBalanceUpdate;
use App\Models\Activity;
use App\Models\CompanyLedger;
@ -36,6 +37,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
return $this;
}
@ -52,6 +54,8 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
return $this;
}
@ -67,6 +71,8 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
return $this;
}

View File

@ -226,7 +226,7 @@ class Design extends BaseDesign
{
if ($this->type === 'statement') {
$s_date = $this->translateDate(now()->format($this->client->company->date_format()), $this->client->date_format(), $this->client->locale());
$s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale());
return [
['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [
@ -391,10 +391,10 @@ class Design extends BaseDesign
$element = ['element' => 'tr', 'elements' => []];
$element['elements'][] = ['element' => 'td', 'content' => $invoice->number];
$element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($invoice->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;'];
$element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($invoice->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;'];
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($invoice->amount, $this->client) ?: '&nbsp;'];
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($invoice->balance, $this->client) ?: '&nbsp;'];
$element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($invoice->date, $this->client->date_format(), $this->client->locale()) ?: ' '];
$element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($invoice->due_date, $this->client->date_format(), $this->client->locale()) ?: ' '];
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($invoice->amount, $this->client) ?: ' '];
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($invoice->balance, $this->client) ?: ' '];
$tbody[] = $element;
}
@ -453,6 +453,9 @@ class Design extends BaseDesign
foreach ($this->invoices as $invoice) {
foreach ($invoice->payments as $payment) {
if($payment->is_deleted)
continue;
$element = ['element' => 'tr', 'elements' => []];
$element['elements'][] = ['element' => 'td', 'content' => $invoice->number];

View File

@ -168,6 +168,9 @@ class CompanyTransformer extends EntityTransformer
'client_registration_fields' => (array) $company->client_registration_fields,
'convert_rate_to_client' => (bool) $company->convert_rate_to_client,
'markdown_email_enabled' => (bool) $company->markdown_email_enabled,
'stop_on_unpaid_recurring' => (bool) $company->stop_on_unpaid_recurring,
'show_production_description_dropdown' => (bool)$company->show_production_description_dropdown,
'use_quote_terms_on_conversion' => (bool) $company->use_quote_terms_on_conversion,
];
}

View File

@ -32,7 +32,11 @@ trait UserNotifies
$notifiable_methods = [];
$notifications = $company_user->notifications;
if ($invitation->company->is_disabled && is_array($notifications->email) || $company_user->trashed() || $company_user->user->trashed()) {
if ($invitation->company->is_disabled &&
is_array($notifications->email) ||
$company_user->trashed() ||
!$company_user->user ||
$company_user->user->trashed()) {
return [];
}
@ -56,7 +60,11 @@ trait UserNotifies
$notifiable_methods = [];
$notifications = $company_user->notifications;
if ($entity->company->is_disabled || ! $notifications || $company_user->trashed() || $company_user->user->trashed()) {
if ($entity->company->is_disabled ||
! $notifications ||
$company_user->trashed() ||
! $company_user->user ||
$company_user->user->trashed()) {
return [];
}
@ -125,7 +133,10 @@ trait UserNotifies
public function findCompanyUserNotificationType($company_user, $required_permissions) :array
{
if ($company_user->company->is_disabled || $company_user->trashed() || $company_user->user->trashed()) {
if ($company_user->company->is_disabled ||
$company_user->trashed() ||
!$company_user->user ||
$company_user->user->trashed()) {
return [];
}

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.3.88',
'app_tag' => '5.3.88',
'app_version' => '5.3.89',
'app_tag' => '5.3.89',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddAutoBillTriesToInvoicesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('invoices', function (Blueprint $table) {
$table->smallInteger('auto_bill_tries')->default(0);
});
Schema::table('companies', function (Blueprint $table) {
$table->boolean('stop_on_unpaid_recurring')->default(0);
$table->boolean('use_quote_terms_on_conversion')->default(0);
$table->boolean('show_production_description_dropdown')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

File diff suppressed because it is too large Load Diff

144
public/flutter.js vendored Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* This script installs service_worker.js to provide PWA functionality to
* application. For more information, see:
* https://developers.google.com/web/fundamentals/primers/service-workers
*/
if (!_flutter) {
var _flutter = {};
}
_flutter.loader = null;
(function() {
"use strict";
class FlutterLoader {
// TODO: Move the below methods to "#private" once supported by all the browsers
// we support. In the meantime, we use the "revealing module" pattern.
// Watchdog to prevent injecting the main entrypoint multiple times.
_scriptLoaded = null;
// Resolver for the pending promise returned by loadEntrypoint.
_didCreateEngineInitializerResolve = null;
/**
* Initializes the main.dart.js with/without serviceWorker.
* @param {*} options
* @returns a Promise that will eventually resolve with an EngineInitializer,
* or will be rejected with the error caused by the loader.
*/
loadEntrypoint(options) {
const {
entrypointUrl = "main.dart.js",
serviceWorker,
} = (options || {});
return this._loadWithServiceWorker(entrypointUrl, serviceWorker);
}
/**
* Resolves the promise created by loadEntrypoint. Called by Flutter.
* Needs to be weirdly bound like it is, so "this" is preserved across
* the JS <-> Flutter jumps.
* @param {*} engineInitializer
*/
didCreateEngineInitializer = (function(engineInitializer) {
if (typeof this._didCreateEngineInitializerResolve != "function") {
console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead.");
}
this._didCreateEngineInitializerResolve(engineInitializer);
// Remove this method after it's done, so Flutter Web can hot restart.
delete this.didCreateEngineInitializer;
}).bind(this);
_loadEntrypoint(entrypointUrl) {
if (!this._scriptLoaded) {
this._scriptLoaded = new Promise((resolve, reject) => {
let scriptTag = document.createElement("script");
scriptTag.src = entrypointUrl;
scriptTag.type = "application/javascript";
this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter.
scriptTag.addEventListener("error", reject);
document.body.append(scriptTag);
});
}
return this._scriptLoaded;
}
_waitForServiceWorkerActivation(serviceWorker, entrypointUrl) {
if (!serviceWorker || serviceWorker.state == "activated") {
if (!serviceWorker) {
console.warn("Cannot activate a null service worker. Falling back to plain <script> tag.");
} else {
console.debug("Service worker already active.");
}
return this._loadEntrypoint(entrypointUrl);
}
return new Promise((resolve, _) => {
serviceWorker.addEventListener("statechange", () => {
if (serviceWorker.state == "activated") {
console.debug("Installed new service worker.");
resolve(this._loadEntrypoint(entrypointUrl));
}
});
});
}
_loadWithServiceWorker(entrypointUrl, serviceWorkerOptions) {
if (!("serviceWorker" in navigator) || serviceWorkerOptions == null) {
console.warn("Service worker not supported (or configured). Falling back to plain <script> tag.", serviceWorkerOptions);
return this._loadEntrypoint(entrypointUrl);
}
const {
serviceWorkerVersion,
timeoutMillis = 4000,
} = serviceWorkerOptions;
let serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
let loader = navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
let sw = reg.installing || reg.waiting;
return this._waitForServiceWorkerActivation(sw, entrypointUrl);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.debug("New service worker available.");
return reg.update().then((reg) => {
console.debug("Service worker updated.");
let sw = reg.installing || reg.waiting || reg.active;
return this._waitForServiceWorkerActivation(sw, entrypointUrl);
});
} else {
// Existing service worker is still good.
console.debug("Loading app from service worker.");
return this._loadEntrypoint(entrypointUrl);
}
});
// Timeout race promise
let timeout;
if (timeoutMillis > 0) {
timeout = new Promise((resolve, _) => {
setTimeout(() => {
if (!this._scriptLoaded) {
console.warn("Failed to load app from service worker. Falling back to plain <script> tag.");
resolve(this._loadEntrypoint(entrypointUrl));
}
}, timeoutMillis);
});
}
return Promise.race([loader, timeout]);
}
}
_flutter.loader = new FlutterLoader();
}());

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"main.dart.js": "405baf11805a6cc292fe5d6ad21109e2",
"main.dart.js": "0b89ccdb131cf8a2d7bdb28e5373c68f",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628",
@ -14,9 +14,9 @@ const RESOURCES = {
"canvaskit/canvaskit.wasm": "4b83d89d9fecbea8ca46f2f760c5a9ba",
"version.json": "3afb81924daf4f751571755436069115",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "b62641afc9ab487008e996a5c5865e56",
"assets/fonts/MaterialIcons-Regular.otf": "7e7a6cccddf6d7b20012a548461d5d81",
"assets/fonts/MaterialIcons-Regular.otf": "95db9098c58fd6db106f1116bae85a0b",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"assets/NOTICES": "e39f4d8d7a45f123298d0dd94ac1ba80",
"assets/NOTICES": "52d7174bb068ef86545951d5bc8c5744",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
@ -37,15 +37,15 @@ const RESOURCES = {
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a",
"/": "11012b191ca75cbf64b79eb3bb3931ef",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",
"/": "d58545047c6b4aa7c8c528dc89852fb6",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40"
};
// The application shell files that are downloaded before a service worker can
// start.
const CORE = [
"/",
"main.dart.js",
"main.dart.js",
"assets/NOTICES",
"assets/AssetManifest.json",
"assets/FontManifest.json"];

379570
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

392972
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

376763
public/main.html.dart.js vendored

File diff suppressed because one or more lines are too long

394458
public/main.next.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

View File

@ -4582,6 +4582,7 @@ $LANG = array(
'auto_archive_cancelled_invoices_help' => 'Automatically archive invoices when they are cancelled.',
'alternate_pdf_viewer' => 'Alternate PDF Viewer',
'alternate_pdf_viewer_help' => 'Improve scrolling over the PDF preview [BETA]',
'currency_cayman_island_dollar' => 'Cayman Island Dollar',
);

View File

@ -105,7 +105,8 @@ Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale
Route::put('invoices/{invoice}/upload', 'InvoiceController@upload')->name('invoices.upload');
Route::get('invoice/{invitation_key}/download', 'InvoiceController@downloadPdf')->name('invoices.downloadPdf');
Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk');
Route::post('invoices/update_reminders', 'InvoiceController@update_reminders')->name('invoices.update_reminders');
Route::post('logout', 'LogoutController@index')->name('logout');
Route::post('migrate', 'MigrationController@index')->name('migrate.start');

View File

@ -169,4 +169,30 @@ class ReminderTest extends TestCase
}
public function testReminderIsSet()
{
$this->invoice->next_send_date = null;
$this->invoice->date = now()->format('Y-m-d');
$this->invoice->due_date = Carbon::now()->addDays(30)->format('Y-m-d');
$this->invoice->save();
$settings = $this->company->settings;
$settings->enable_reminder1 = true;
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->num_days_reminder1 = 7;
$settings->enable_reminder2 = true;
$settings->schedule_reminder2 = 'before_due_date';
$settings->num_days_reminder2 = 1;
$settings->enable_reminder3 = true;
$settings->schedule_reminder3 = 'after_due_date';
$settings->num_days_reminder3 = 1;
$this->company->settings = $settings;
$this->invoice = $this->invoice->service()->markSent()->save();
$this->invoice->service()->setReminder($settings)->save();
$this->assertNotNull($this->invoice->next_send_date);
}
}