mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 18:54:30 -04:00
Merge pull request #6431 from turbo124/v5-develop
Email Quotas for hosted
This commit is contained in:
commit
c663369d38
@ -69,7 +69,7 @@ class Kernel extends ConsoleKernel
|
|||||||
/* Run hosted specific jobs */
|
/* Run hosted specific jobs */
|
||||||
if (Ninja::isHosted()) {
|
if (Ninja::isHosted()) {
|
||||||
|
|
||||||
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
|
$schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
|
||||||
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
|
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
|
||||||
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();
|
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice Ninja (https://invoiceninja.com).
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
*
|
*
|
||||||
@ -10,6 +9,8 @@
|
|||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use App\Utils\Ninja;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple helper function that will log into "invoiceninja.log" file
|
* Simple helper function that will log into "invoiceninja.log" file
|
||||||
* only when extended logging is enabled.
|
* only when extended logging is enabled.
|
||||||
@ -32,7 +33,10 @@ function nlog($output, $context = []): void
|
|||||||
$trace = debug_backtrace();
|
$trace = debug_backtrace();
|
||||||
//nlog( debug_backtrace()[1]['function']);
|
//nlog( debug_backtrace()[1]['function']);
|
||||||
// \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []);
|
// \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []);
|
||||||
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
|
if(Ninja::isHosted())
|
||||||
|
info($output);
|
||||||
|
else
|
||||||
|
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,11 +730,11 @@ class BaseController extends Controller
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
if (Ninja::isSelfHost()) {
|
//pass report errors bool to front end
|
||||||
$data['report_errors'] = $account->report_errors;
|
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
|
||||||
} else {
|
|
||||||
$data['report_errors'] = true;
|
//pass referral code to front end
|
||||||
}
|
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
|
||||||
|
|
||||||
$this->buildCache();
|
$this->buildCache();
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\DataMapper\Analytics\EmailBounce;
|
use App\DataMapper\Analytics\EmailBounce;
|
||||||
use App\DataMapper\Analytics\EmailSpam;
|
use App\DataMapper\Analytics\Mail\EmailSpam;
|
||||||
use App\Jobs\Util\SystemLogger;
|
use App\Jobs\Util\SystemLogger;
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use App\Models\CreditInvitation;
|
use App\Models\CreditInvitation;
|
||||||
|
@ -71,7 +71,8 @@ class CreateAccount
|
|||||||
$sp794f3f = new Account();
|
$sp794f3f = new Account();
|
||||||
$sp794f3f->fill($this->request);
|
$sp794f3f->fill($this->request);
|
||||||
|
|
||||||
$sp794f3f->referral_code = Str::random(32);
|
if(array_key_exists('rc', $this->request))
|
||||||
|
$sp794f3f->referral_code = $this->request['rc'];
|
||||||
|
|
||||||
if (! $sp794f3f->key) {
|
if (! $sp794f3f->key) {
|
||||||
$sp794f3f->key = Str::random(32);
|
$sp794f3f->key = Str::random(32);
|
||||||
|
@ -40,6 +40,7 @@ use Illuminate\Support\Facades\Config;
|
|||||||
use Illuminate\Support\Facades\Lang;
|
use Illuminate\Support\Facades\Lang;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Turbo124\Beacon\Facades\LightLogs;
|
use Turbo124\Beacon\Facades\LightLogs;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
/*Multi Mailer implemented*/
|
/*Multi Mailer implemented*/
|
||||||
|
|
||||||
@ -71,9 +72,6 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
|
|
||||||
if($this->preFlightChecksFail())
|
|
||||||
return;
|
|
||||||
|
|
||||||
/*Set the correct database*/
|
/*Set the correct database*/
|
||||||
MultiDB::setDb($this->nmo->company->db);
|
MultiDB::setDb($this->nmo->company->db);
|
||||||
@ -81,6 +79,9 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
/* Serializing models from other jobs wipes the primary key */
|
/* Serializing models from other jobs wipes the primary key */
|
||||||
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
|
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
|
||||||
|
|
||||||
|
if($this->preFlightChecksFail())
|
||||||
|
return;
|
||||||
|
|
||||||
/* Set the email driver */
|
/* Set the email driver */
|
||||||
$this->setMailDriver();
|
$this->setMailDriver();
|
||||||
|
|
||||||
@ -110,11 +111,10 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
|
LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
|
||||||
->batch();
|
->batch();
|
||||||
|
|
||||||
|
/* Count the amount of emails sent across all the users accounts */
|
||||||
|
Cache::increment($this->company->account->key);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
// if($e instanceof GuzzleHttp\Exception\ClientException){
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
nlog("error failed with {$e->getMessage()}");
|
nlog("error failed with {$e->getMessage()}");
|
||||||
|
|
||||||
@ -227,6 +227,11 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
|
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */
|
||||||
|
if(Ninja::isHosted() && $this->company->account->emailQuotaExceeded())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,4 +259,5 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
LightLogs::create($job_failure)
|
LightLogs::create($job_failure)
|
||||||
->batch();
|
->batch();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
@ -13,26 +13,18 @@ namespace App\Jobs\Ninja;
|
|||||||
|
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use App\Models\Account;
|
use App\Models\Account;
|
||||||
|
use App\Utils\Ninja;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class AdjustEmailQuota implements ShouldQueue
|
class AdjustEmailQuota implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
const FREE_PLAN_DAILY_QUOTA = 10;
|
|
||||||
const PRO_PLAN_DAILY_QUOTA = 50;
|
|
||||||
const ENTERPRISE_PLAN_DAILY_QUOTA = 200;
|
|
||||||
|
|
||||||
const FREE_PLAN_DAILY_CAP = 20;
|
|
||||||
const PRO_PLAN_DAILY_CAP = 100;
|
|
||||||
const ENTERPRISE_PLAN_DAILY_CAP = 300;
|
|
||||||
|
|
||||||
const DAILY_MULTIPLIER = 1.1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*
|
*
|
||||||
@ -50,22 +42,27 @@ class AdjustEmailQuota implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
if (! config('ninja.db.multi_db_enabled')) {
|
if(!Ninja::isHosted())
|
||||||
$this->adjust();
|
return;
|
||||||
} else {
|
|
||||||
//multiDB environment, need to
|
//multiDB environment, need to
|
||||||
foreach (MultiDB::$dbs as $db) {
|
foreach (MultiDB::$dbs as $db) {
|
||||||
MultiDB::setDB($db);
|
|
||||||
|
MultiDB::setDB($db);
|
||||||
|
|
||||||
|
$this->adjust();
|
||||||
|
|
||||||
$this->adjust();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function adjust()
|
public function adjust()
|
||||||
{
|
{
|
||||||
foreach (Account::cursor() as $account) {
|
|
||||||
//@TODO once we add in the two columns daily_emails_quota daily_emails_sent_
|
Account::query()->cursor()->each(function ($account){
|
||||||
}
|
Cache::forget($account->key);
|
||||||
|
Cache::forget("throttle_notified:{$account->key}");
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ class ReminderJob implements ShouldQueue
|
|||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates;
|
||||||
|
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -48,6 +50,7 @@ class ReminderJob implements ShouldQueue
|
|||||||
//multiDB environment, need to
|
//multiDB environment, need to
|
||||||
foreach (MultiDB::$dbs as $db) {
|
foreach (MultiDB::$dbs as $db) {
|
||||||
MultiDB::setDB($db);
|
MultiDB::setDB($db);
|
||||||
|
nlog("set db {$db}");
|
||||||
$this->processReminders();
|
$this->processReminders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
app/Mail/Ninja/EmailQuotaExceeded.php
Normal file
61
app/Mail/Ninja/EmailQuotaExceeded.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Mail\Ninja;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class EmailQuotaExceeded extends Mailable
|
||||||
|
{
|
||||||
|
|
||||||
|
public $company;
|
||||||
|
|
||||||
|
public $settings;
|
||||||
|
|
||||||
|
public $logo;
|
||||||
|
|
||||||
|
public $title;
|
||||||
|
|
||||||
|
public $body;
|
||||||
|
|
||||||
|
public $whitelabel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($company)
|
||||||
|
{
|
||||||
|
$this->company = $company;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
$this->settings = $this->company->settings;
|
||||||
|
$this->logo = $this->company->present()->logo();
|
||||||
|
$this->title = ctrans('texts.email_quota_exceeded_subject');
|
||||||
|
$this->body = ctrans('texts.email_quota_exceeded_body', ['quota' => $this->company->account->getDailyEmailLimit()]);
|
||||||
|
$this->whitelabel = $this->company->account->isPaid();
|
||||||
|
$this->replyTo('contact@invoiceninja.com', 'Contact');
|
||||||
|
|
||||||
|
return $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||||
|
->subject(ctrans('texts.email_quota_exceeded_subject'))
|
||||||
|
->view('email.admin.email_quota_exceeded');
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,16 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Jobs\Mail\NinjaMailerJob;
|
||||||
|
use App\Jobs\Mail\NinjaMailerObject;
|
||||||
|
use App\Mail\Ninja\EmailQuotaExceeded;
|
||||||
use App\Models\Presenters\AccountPresenter;
|
use App\Models\Presenters\AccountPresenter;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Laracasts\Presenter\PresentableTrait;
|
use Laracasts\Presenter\PresentableTrait;
|
||||||
|
|
||||||
class Account extends BaseModel
|
class Account extends BaseModel
|
||||||
@ -24,6 +28,9 @@ class Account extends BaseModel
|
|||||||
use PresentableTrait;
|
use PresentableTrait;
|
||||||
use MakesHash;
|
use MakesHash;
|
||||||
|
|
||||||
|
private $free_plan_email_quota = 250;
|
||||||
|
|
||||||
|
private $paid_plan_email_quota = 500;
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
@ -341,4 +348,45 @@ class Account extends BaseModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDailyEmailLimit()
|
||||||
|
{
|
||||||
|
|
||||||
|
if($this->isPaid()){
|
||||||
|
$limit = $this->paid_plan_email_quota;
|
||||||
|
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$limit = $this->free_plan_email_quota;
|
||||||
|
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min($limit, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function emailQuotaExceeded() :bool
|
||||||
|
{
|
||||||
|
if(is_null(Cache::get($this->key)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(Cache::get($this->key) > $this->getDailyEmailLimit()) {
|
||||||
|
|
||||||
|
if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
|
||||||
|
|
||||||
|
$nmo = new NinjaMailerObject;
|
||||||
|
$nmo->mailable = new EmailQuotaExceeded($this->companies()->first());
|
||||||
|
$nmo->company = $this->companies()->first();
|
||||||
|
$nmo->settings = $this->companies()->first()->settings;
|
||||||
|
$nmo->to_user = $this->companies()->first()->owner();
|
||||||
|
NinjaMailerJob::dispatch($nmo);
|
||||||
|
|
||||||
|
Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@ class Company extends BaseModel
|
|||||||
protected $presenter = CompanyPresenter::class;
|
protected $presenter = CompanyPresenter::class;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
'markdown_enabled',
|
||||||
'calculate_expense_tax_by_amount',
|
'calculate_expense_tax_by_amount',
|
||||||
'invoice_expense_documents',
|
'invoice_expense_documents',
|
||||||
'invoice_task_documents',
|
'invoice_task_documents',
|
||||||
|
@ -113,4 +113,107 @@ class SystemLog extends Model
|
|||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCategoryName()
|
||||||
|
{
|
||||||
|
switch ($this->category_id) {
|
||||||
|
case self::CATEGORY_GATEWAY_RESPONSE:
|
||||||
|
return "Gateway";
|
||||||
|
case self::CATEGORY_MAIL:
|
||||||
|
return "Mail";
|
||||||
|
case self::CATEGORY_WEBHOOK:
|
||||||
|
return "Webhook";
|
||||||
|
case self::CATEGORY_PDF:
|
||||||
|
return "PDF";
|
||||||
|
case self::CATEGORY_SECURITY:
|
||||||
|
return "Security";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'undefined';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventName()
|
||||||
|
{
|
||||||
|
switch ($this->event_id) {
|
||||||
|
case self::EVENT_PAYMENT_RECONCILIATION_FAILURE:
|
||||||
|
return "Payment reco failure";
|
||||||
|
case self::EVENT_PAYMENT_RECONCILIATION_SUCCESS:
|
||||||
|
return "Payment reco success";
|
||||||
|
case self::EVENT_GATEWAY_SUCCESS:
|
||||||
|
return "Success";
|
||||||
|
case self::EVENT_GATEWAY_FAILURE:
|
||||||
|
return "Failure";
|
||||||
|
case self::EVENT_GATEWAY_ERROR:
|
||||||
|
return "Error";
|
||||||
|
case self::EVENT_MAIL_SEND:
|
||||||
|
return "Send";
|
||||||
|
case self::EVENT_MAIL_RETRY_QUEUE:
|
||||||
|
return "Retry";
|
||||||
|
case self::EVENT_MAIL_BOUNCED:
|
||||||
|
return "Bounced";
|
||||||
|
case self::EVENT_MAIL_SPAM_COMPLAINT:
|
||||||
|
return "Spam";
|
||||||
|
case self::EVENT_MAIL_DELIVERY:
|
||||||
|
return "Delivery";
|
||||||
|
case self::EVENT_WEBHOOK_RESPONSE:
|
||||||
|
return "Webhook Response";
|
||||||
|
case self::EVENT_PDF_RESPONSE:
|
||||||
|
return "Pdf Response";
|
||||||
|
case self::EVENT_AUTHENTICATION_FAILURE:
|
||||||
|
return "Auth Failure";
|
||||||
|
case self::EVENT_USER:
|
||||||
|
return "User";
|
||||||
|
default:
|
||||||
|
return 'undefined';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeName()
|
||||||
|
{
|
||||||
|
switch ($this->type_id) {
|
||||||
|
case self::TYPE_QUOTA_EXCEEDED:
|
||||||
|
return "Quota Exceeded";
|
||||||
|
case self::TYPE_UPSTREAM_FAILURE:
|
||||||
|
return "Upstream Failure";
|
||||||
|
case self::TYPE_WEBHOOK_RESPONSE:
|
||||||
|
return "Webhook";
|
||||||
|
case self::TYPE_PDF_FAILURE:
|
||||||
|
return "Failure";
|
||||||
|
case self::TYPE_PDF_SUCCESS:
|
||||||
|
return "Success";
|
||||||
|
case self::TYPE_MODIFIED:
|
||||||
|
return "Modified";
|
||||||
|
case self::TYPE_DELETED:
|
||||||
|
return "Deleted";
|
||||||
|
case self::TYPE_LOGIN_SUCCESS:
|
||||||
|
return "Login Success";
|
||||||
|
case self::TYPE_LOGIN_FAILURE:
|
||||||
|
return "Login Failure";
|
||||||
|
case self::TYPE_PAYPAL:
|
||||||
|
return "PayPal";
|
||||||
|
case self::TYPE_STRIPE:
|
||||||
|
return "Stripe";
|
||||||
|
case self::TYPE_LEDGER:
|
||||||
|
return "Ledger";
|
||||||
|
case self::TYPE_FAILURE:
|
||||||
|
return "Failure";
|
||||||
|
case self::TYPE_CHECKOUT:
|
||||||
|
return "Checkout";
|
||||||
|
case self::TYPE_AUTHORIZE:
|
||||||
|
return "Auth.net";
|
||||||
|
case self::TYPE_CUSTOM:
|
||||||
|
return "Custom";
|
||||||
|
case self::TYPE_BRAINTREE:
|
||||||
|
return "Braintree";
|
||||||
|
case self::TYPE_WEPAY:
|
||||||
|
return "WePay";
|
||||||
|
case self::TYPE_PAYFAST:
|
||||||
|
return "Payfast";
|
||||||
|
default:
|
||||||
|
return 'undefined';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ class ApplyPayment extends AbstractService
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->invoice->service()->applyNumber()->save();
|
$this->invoice->service()->applyNumber()->workFlow()->save();
|
||||||
|
|
||||||
return $this->invoice;
|
return $this->invoice;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ use App\Models\Expense;
|
|||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
|
use App\Repositories\BaseRepository;
|
||||||
use App\Services\Client\ClientService;
|
use App\Services\Client\ClientService;
|
||||||
use App\Services\Invoice\UpdateReminder;
|
use App\Services\Invoice\UpdateReminder;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
@ -271,9 +272,8 @@ class InvoiceService
|
|||||||
{
|
{
|
||||||
if ((int)$this->invoice->balance == 0) {
|
if ((int)$this->invoice->balance == 0) {
|
||||||
|
|
||||||
InvoiceWorkflowSettings::dispatchNow($this->invoice);
|
$this->setStatus(Invoice::STATUS_PAID)->workFlow();
|
||||||
|
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
|
||||||
$this->setStatus(Invoice::STATUS_PAID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
|
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
|
||||||
@ -449,6 +449,18 @@ class InvoiceService
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function workFlow()
|
||||||
|
{
|
||||||
|
|
||||||
|
if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) {
|
||||||
|
/* Throws: Payment amount xxx does not match invoice totals. */
|
||||||
|
$base_repository = new BaseRepository();
|
||||||
|
$base_repository->archive($this->invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the invoice.
|
* Saves the invoice.
|
||||||
* @return Invoice object
|
* @return Invoice object
|
||||||
|
@ -95,7 +95,8 @@ class MarkPaid extends AbstractService
|
|||||||
->updatePaidToDate($payment->amount)
|
->updatePaidToDate($payment->amount)
|
||||||
->save();
|
->save();
|
||||||
|
|
||||||
InvoiceWorkflowSettings::dispatchNow($this->invoice);
|
$this->invoice->service()->workFlow()->save();
|
||||||
|
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
|
||||||
|
|
||||||
return $this->invoice;
|
return $this->invoice;
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ class RefundPayment
|
|||||||
|
|
||||||
if (isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) {
|
if (isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) {
|
||||||
foreach ($this->refund_data['invoices'] as $refunded_invoice) {
|
foreach ($this->refund_data['invoices'] as $refunded_invoice) {
|
||||||
$invoice = Invoice::find($refunded_invoice['invoice_id']);
|
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
|
||||||
|
|
||||||
$invoice->service()->updateBalance($refunded_invoice['amount'])->save();
|
$invoice->service()->updateBalance($refunded_invoice['amount'])->save();
|
||||||
$invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();
|
$invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();
|
||||||
|
@ -85,8 +85,6 @@ class UpdateInvoicePayment
|
|||||||
->deletePdf()
|
->deletePdf()
|
||||||
->save();
|
->save();
|
||||||
|
|
||||||
InvoiceWorkflowSettings::dispatchNow($invoice);
|
|
||||||
|
|
||||||
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,6 +159,7 @@ class CompanyTransformer extends EntityTransformer
|
|||||||
'default_password_timeout' => (int) $company->default_password_timeout,
|
'default_password_timeout' => (int) $company->default_password_timeout,
|
||||||
'invoice_task_datelog' => (bool) $company->invoice_task_datelog,
|
'invoice_task_datelog' => (bool) $company->invoice_task_datelog,
|
||||||
'show_task_end_date' => (bool) $company->show_task_end_date,
|
'show_task_end_date' => (bool) $company->show_task_end_date,
|
||||||
|
'markdown_enabled' => (bool) $company->markdown_enabled,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddMarkdownEnabledColumnToCompaniesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('companies', function (Blueprint $table) {
|
||||||
|
$table->boolean('markdown_enabled')->default(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
13287
package-lock.json
generated
13287
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@
|
|||||||
"card-validator": "^6.2.0",
|
"card-validator": "^6.2.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jsignature": "^2.1.3",
|
"jsignature": "^2.1.3",
|
||||||
|
"json-formatter-js": "^2.3.4",
|
||||||
"laravel-mix": "^5.0.9",
|
"laravel-mix": "^5.0.9",
|
||||||
"linkify-urls": "^3.1.1",
|
"linkify-urls": "^3.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
194
public/js/jsformatter.css
vendored
Normal file
194
public/js/jsformatter.css
vendored
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
.json-formatter-row {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.json-formatter-row,
|
||||||
|
.json-formatter-row a,
|
||||||
|
.json-formatter-row a:hover {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-row {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-children.json-formatter-empty {
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-children.json-formatter-empty:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
|
||||||
|
content: "No properties";
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
|
||||||
|
content: "[]";
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-string,
|
||||||
|
.json-formatter-row .json-formatter-stringifiable {
|
||||||
|
color: green;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-number {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-boolean {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-null {
|
||||||
|
color: #855A00;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-undefined {
|
||||||
|
color: #ca0b69;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-function {
|
||||||
|
color: #FF20ED;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-date {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-url {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-bracket {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-key {
|
||||||
|
color: #00008B;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-toggler-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-toggler {
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
.json-formatter-row .json-formatter-toggler:after {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 100ms ease-in;
|
||||||
|
content: "►";
|
||||||
|
}
|
||||||
|
.json-formatter-row > a > .json-formatter-preview-text {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease-in;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.json-formatter-row:hover > a > .json-formatter-preview-text {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.json-formatter-row.json-formatter-open > .json-formatter-children:after {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.json-formatter-row.json-formatter-open.json-formatter-empty:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row,
|
||||||
|
.json-formatter-dark.json-formatter-row a,
|
||||||
|
.json-formatter-dark.json-formatter-row a:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-row {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty {
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
|
||||||
|
content: "No properties";
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
|
||||||
|
content: "[]";
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-string,
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-stringifiable {
|
||||||
|
color: #31F031;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-number {
|
||||||
|
color: #66C2FF;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-boolean {
|
||||||
|
color: #EC4242;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-null {
|
||||||
|
color: #EEC97D;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-undefined {
|
||||||
|
color: #ef8fbe;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-function {
|
||||||
|
color: #FD48CB;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-date {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-url {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #027BFF;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-bracket {
|
||||||
|
color: #9494FF;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-key {
|
||||||
|
color: #23A0DB;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-toggler-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-toggler {
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row .json-formatter-toggler:after {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 100ms ease-in;
|
||||||
|
content: "►";
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row > a > .json-formatter-preview-text {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease-in;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row:hover > a > .json-formatter-preview-text {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-children:after {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.json-formatter-dark.json-formatter-row.json-formatter-open.json-formatter-empty:after {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -4289,6 +4289,8 @@ $LANG = array(
|
|||||||
'back_to' => 'Back to :url',
|
'back_to' => 'Back to :url',
|
||||||
'stripe_connect_migration_title' => 'Connect your Stripe Account',
|
'stripe_connect_migration_title' => 'Connect your Stripe Account',
|
||||||
'stripe_connect_migration_desc' => 'Invoice Ninja v5 uses Stripe Connect to link your Stripe account to Invoice Ninja. This provides an additional layer of security for your account. Now that you data has migrated, you will need to Authorize Stripe to accept payments in v5.<br><br>To do this, navigate to Settings > Online Payments > Configure Gateways. Click on Stripe Connect and then under Settings click Setup Gateway. This will take you to Stripe to authorize Invoice Ninja and on your return your account will be successfully linked!',
|
'stripe_connect_migration_desc' => 'Invoice Ninja v5 uses Stripe Connect to link your Stripe account to Invoice Ninja. This provides an additional layer of security for your account. Now that you data has migrated, you will need to Authorize Stripe to accept payments in v5.<br><br>To do this, navigate to Settings > Online Payments > Configure Gateways. Click on Stripe Connect and then under Settings click Setup Gateway. This will take you to Stripe to authorize Invoice Ninja and on your return your account will be successfully linked!',
|
||||||
|
'email_quota_exceeded_subject' => 'Account email quota exceeded.',
|
||||||
|
'email_quota_exceeded_body' => 'In a 24 hour period you have sent :quota emails. <br> We have paused your outbound emails.<br><br> Your email quota will reset at 23:00 UTC.',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $LANG;
|
return $LANG;
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
@component('email.template.admin', ['logo' => $logo, 'settings' => $settings])
|
||||||
|
<div class="center">
|
||||||
|
<h1>{!! $title !!}</h1>
|
||||||
|
|
||||||
|
<p>{!! $body !!}</p>
|
||||||
|
</div>
|
||||||
|
@endcomponent
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-report-errors="{{ $report_errors }}">
|
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}">
|
||||||
<head>
|
<head>
|
||||||
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
|
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
|
||||||
<!-- Version: {{ config('ninja.app_version') }} -->
|
<!-- Version: {{ config('ninja.app_version') }} -->
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
<select name="mail_driver" class="input w-full form-select" x-model="option">
|
<select name="mail_driver" class="input w-full form-select" x-model="option">
|
||||||
<option value="log">Log</option>
|
<option value="log">Log</option>
|
||||||
<option value="smtp">SMTP</option>
|
<option value="smtp">SMTP</option>
|
||||||
<option value="mail">Mail</option>
|
|
||||||
<option value="sendmail">Sendmail</option>
|
<option value="sendmail">Sendmail</option>
|
||||||
</select>
|
</select>
|
||||||
</dd>
|
</dd>
|
||||||
|
62
tests/Feature/Account/AccountEmailQuotaTest.php
Normal file
62
tests/Feature/Account/AccountEmailQuotaTest.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Feature\Account;
|
||||||
|
|
||||||
|
use App\DataMapper\ClientSettings;
|
||||||
|
use App\DataMapper\CompanySettings;
|
||||||
|
use App\Http\Livewire\CreditsTable;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Credit;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Utils\Traits\AppSetup;
|
||||||
|
use Faker\Factory;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class AccountEmailQuotaTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
use AppSetup;
|
||||||
|
use MockAccountData;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->faker = Factory::create();
|
||||||
|
$this->buildCache(true);
|
||||||
|
$this->makeTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQuotaValidRule()
|
||||||
|
{
|
||||||
|
Cache::increment($this->account->key);
|
||||||
|
|
||||||
|
$this->assertFalse($this->account->emailQuotaExceeded());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testQuotaInValidRule()
|
||||||
|
{
|
||||||
|
Cache::increment($this->account->key, 3000);
|
||||||
|
|
||||||
|
$this->assertTrue($this->account->emailQuotaExceeded());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user