Merge pull request #6431 from turbo124/v5-develop

Email Quotas for hosted
This commit is contained in:
David Bomba 2021-08-09 20:21:35 +10:00 committed by GitHub
commit c663369d38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1144 additions and 12772 deletions

View File

@ -69,7 +69,7 @@ class Kernel extends ConsoleKernel
/* Run hosted specific jobs */
if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
$schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();

View File

@ -1,5 +1,4 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -10,6 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
use App\Utils\Ninja;
/**
* Simple helper function that will log into "invoiceninja.log" file
* only when extended logging is enabled.
@ -32,7 +33,10 @@ function nlog($output, $context = []): void
$trace = debug_backtrace();
//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($output, $context);
if(Ninja::isHosted())
info($output);
else
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
}

View File

@ -730,11 +730,11 @@ class BaseController extends Controller
$data = [];
if (Ninja::isSelfHost()) {
$data['report_errors'] = $account->report_errors;
} else {
$data['report_errors'] = true;
}
//pass report errors bool to front end
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
//pass referral code to front end
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$this->buildCache();

View File

@ -12,7 +12,7 @@
namespace App\Http\Controllers;
use App\DataMapper\Analytics\EmailBounce;
use App\DataMapper\Analytics\EmailSpam;
use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\CreditInvitation;

View File

@ -71,7 +71,8 @@ class CreateAccount
$sp794f3f = new Account();
$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) {
$sp794f3f->key = Str::random(32);

View File

@ -40,6 +40,7 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Cache;
/*Multi Mailer implemented*/
@ -72,15 +73,15 @@ class NinjaMailerJob implements ShouldQueue
public function handle()
{
if($this->preFlightChecksFail())
return;
/*Set the correct database*/
MultiDB::setDb($this->nmo->company->db);
/* Serializing models from other jobs wipes the primary key */
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
if($this->preFlightChecksFail())
return;
/* Set the email driver */
$this->setMailDriver();
@ -110,12 +111,11 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
->batch();
/* Count the amount of emails sent across all the users accounts */
Cache::increment($this->company->account->key);
} catch (\Exception $e) {
// if($e instanceof GuzzleHttp\Exception\ClientException){
// }
nlog("error failed with {$e->getMessage()}");
if($this->nmo->entity)
@ -227,6 +227,11 @@ class NinjaMailerJob implements ShouldQueue
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
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;
}
@ -254,4 +259,5 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create($job_failure)
->batch();
}
}

View File

@ -13,26 +13,18 @@ namespace App\Jobs\Ninja;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Utils\Ninja;
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\Cache;
class AdjustEmailQuota implements ShouldQueue
{
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.
*
@ -50,22 +42,27 @@ class AdjustEmailQuota implements ShouldQueue
*/
public function handle()
{
if (! config('ninja.db.multi_db_enabled')) {
$this->adjust();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
if(!Ninja::isHosted())
return;
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->adjust();
$this->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}");
});
}
}

View File

@ -30,6 +30,8 @@ class ReminderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates;
public $tries = 1;
public function __construct()
{
}
@ -48,6 +50,7 @@ class ReminderJob implements ShouldQueue
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
}

View 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');
}
}

View File

@ -11,12 +11,16 @@
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\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use DateTime;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait;
class Account extends BaseModel
@ -24,6 +28,9 @@ class Account extends BaseModel
use PresentableTrait;
use MakesHash;
private $free_plan_email_quota = 250;
private $paid_plan_email_quota = 500;
/**
* @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;
}
}

View File

@ -50,6 +50,7 @@ class Company extends BaseModel
protected $presenter = CompanyPresenter::class;
protected $fillable = [
'markdown_enabled',
'calculate_expense_tax_by_amount',
'invoice_expense_documents',
'invoice_task_documents',

View File

@ -113,4 +113,107 @@ class SystemLog extends Model
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';
}
}
}

View File

@ -126,7 +126,7 @@ class ApplyPayment extends AbstractService
});
$this->invoice->service()->applyNumber()->save();
$this->invoice->service()->applyNumber()->workFlow()->save();
return $this->invoice;
}

View File

@ -20,6 +20,7 @@ use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Task;
use App\Repositories\BaseRepository;
use App\Services\Client\ClientService;
use App\Services\Invoice\UpdateReminder;
use App\Utils\Ninja;
@ -271,9 +272,8 @@ class InvoiceService
{
if ((int)$this->invoice->balance == 0) {
InvoiceWorkflowSettings::dispatchNow($this->invoice);
$this->setStatus(Invoice::STATUS_PAID);
$this->setStatus(Invoice::STATUS_PAID)->workFlow();
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
}
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
@ -449,6 +449,18 @@ class InvoiceService
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.
* @return Invoice object

View File

@ -95,7 +95,8 @@ class MarkPaid extends AbstractService
->updatePaidToDate($payment->amount)
->save();
InvoiceWorkflowSettings::dispatchNow($this->invoice);
$this->invoice->service()->workFlow()->save();
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
return $this->invoice;
}

View File

@ -232,7 +232,7 @@ class RefundPayment
if (isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) {
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->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();

View File

@ -85,8 +85,6 @@ class UpdateInvoicePayment
->deletePdf()
->save();
InvoiceWorkflowSettings::dispatchNow($invoice);
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
});

View File

@ -159,6 +159,7 @@ class CompanyTransformer extends EntityTransformer
'default_password_timeout' => (int) $company->default_password_timeout,
'invoice_task_datelog' => (bool) $company->invoice_task_datelog,
'show_task_end_date' => (bool) $company->show_task_end_date,
'markdown_enabled' => (bool) $company->markdown_enabled,
];
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
"card-validator": "^6.2.0",
"cross-env": "^7.0.3",
"jsignature": "^2.1.3",
"json-formatter-js": "^2.3.4",
"laravel-mix": "^5.0.9",
"linkify-urls": "^3.1.1",
"lodash": "^4.17.21",

194
public/js/jsformatter.css vendored Normal file
View 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;
}

View File

@ -4289,6 +4289,8 @@ $LANG = array(
'back_to' => 'Back to :url',
'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!',
'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;

View File

@ -0,0 +1,7 @@
@component('email.template.admin', ['logo' => $logo, 'settings' => $settings])
<div class="center">
<h1>{!! $title !!}</h1>
<p>{!! $body !!}</p>
</div>
@endcomponent

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}">
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} -->

View File

@ -17,7 +17,6 @@
<select name="mail_driver" class="input w-full form-select" x-model="option">
<option value="log">Log</option>
<option value="smtp">SMTP</option>
<option value="mail">Mail</option>
<option value="sendmail">Sendmail</option>
</select>
</dd>

View 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());
}
}