Implement invoice reminder scheduler (#3160)

* Do not set email template defaults in settings

* Set invoice reminders, trait + tets

* Fixes for tets

* Only all an account owner to delete companies/users
This commit is contained in:
David Bomba 2019-12-18 13:45:18 +11:00 committed by GitHub
parent 556b2ab1c9
commit e125052f96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 381 additions and 20 deletions

View File

@ -410,22 +410,22 @@ class CompanySettings extends BaseSettings
$data->country_id = (string)config('ninja.i18n.country_id');
$data->translations = (object) [];
$data->email_subject_invoice = EmailTemplateDefaults::emailInvoiceSubject();
$data->email_template_invoice = EmailTemplateDefaults:: emailInvoiceTemplate();
$data->email_subject_quote = EmailTemplateDefaults::emailQuoteSubject();
$data->email_subject_payment = EmailTemplateDefaults::emailPaymentSubject();
$data->email_subject_statement = EmailTemplateDefaults::emailStatementSubject();
$data->email_template_quote = EmailTemplateDefaults::emailQuoteTemplate();
$data->email_template_payment = EmailTemplateDefaults::emailPaymentTemplate();
$data->email_template_statement = EmailTemplateDefaults::emailStatementTemplate();
$data->email_subject_reminder1 = EmailTemplateDefaults::emailReminder1Subject();
$data->email_subject_reminder2 = EmailTemplateDefaults::emailReminder2Subject();
$data->email_subject_reminder3 = EmailTemplateDefaults::emailReminder3Subject();
$data->email_subject_reminder_endless = EmailTemplateDefaults::emailReminderEndlessSubject();
$data->email_template_reminder1 = EmailTemplateDefaults::emailReminder1Template();
$data->email_template_reminder2 = EmailTemplateDefaults::emailReminder2Template();
$data->email_template_reminder3 = EmailTemplateDefaults::emailReminder3Template();
$data->email_template_reminder_endless = EmailTemplateDefaults::emailReminderEndlessTemplate();
// $data->email_subject_invoice = EmailTemplateDefaults::emailInvoiceSubject();
// $data->email_template_invoice = EmailTemplateDefaults:: emailInvoiceTemplate();
// $data->email_subject_quote = EmailTemplateDefaults::emailQuoteSubject();
// $data->email_subject_payment = EmailTemplateDefaults::emailPaymentSubject();
// $data->email_subject_statement = EmailTemplateDefaults::emailStatementSubject();
// $data->email_template_quote = EmailTemplateDefaults::emailQuoteTemplate();
// $data->email_template_payment = EmailTemplateDefaults::emailPaymentTemplate();
// $data->email_template_statement = EmailTemplateDefaults::emailStatementTemplate();
// $data->email_subject_reminder1 = EmailTemplateDefaults::emailReminder1Subject();
// $data->email_subject_reminder2 = EmailTemplateDefaults::emailReminder2Subject();
// $data->email_subject_reminder3 = EmailTemplateDefaults::emailReminder3Subject();
// $data->email_subject_reminder_endless = EmailTemplateDefaults::emailReminderEndlessSubject();
// $data->email_template_reminder1 = EmailTemplateDefaults::emailReminder1Template();
// $data->email_template_reminder2 = EmailTemplateDefaults::emailReminder2Template();
// $data->email_template_reminder3 = EmailTemplateDefaults::emailReminder3Template();
// $data->email_template_reminder_endless = EmailTemplateDefaults::emailReminderEndlessTemplate();
return self::setCasts($data, self::$casts);

View File

@ -24,7 +24,7 @@ class DestroyCompanyRequest extends Request
public function authorize() : bool
{
return auth()->user()->can('edit', $this->company);
return auth()->user()->isOwner();
}
}

View File

@ -24,7 +24,7 @@ class DestroyUserRequest extends Request
public function authorize() : bool
{
return auth()->user()->can('edit', $this->user);
return auth()->user()->isOwner();
}
}

View File

@ -182,7 +182,7 @@ class Client extends BaseModel
public function locale()
{
return $this->language()->locale;
return $this->language()->locale ?: 'en';
}
public function date_format()

View File

@ -25,6 +25,7 @@ use App\Utils\Number;
use App\Utils\Traits\InvoiceEmailBuilder;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -43,7 +44,8 @@ class Invoice extends BaseModel
use PresentableTrait;
use MakesInvoiceValues;
use InvoiceEmailBuilder;
use MakesReminders;
protected $presenter = 'App\Models\Presenters\InvoicePresenter';
protected $hidden = [
@ -439,6 +441,8 @@ class Invoice extends BaseModel
$this->markInvitationsSent();
$this->setReminder();
event(new InvoiceWasMarkedSent($this));
UpdateClientBalance::dispatchNow($this->client, $this->balance);

View File

@ -235,6 +235,13 @@ class User extends Authenticatable implements MustVerifyEmail
}
public function isOwner() : bool
{
return $this->company_user->is_owner;
}
/**
* Returns all user created contacts
*

View File

@ -48,8 +48,24 @@ trait InvoiceEmailBuilder
$client = $this->client;
$body_template = $client->getSetting('email_template_'.$reminder_template);
/* Use default translations if a custom message has not been set*/
if(iconv_strlen($body_template) == 0){
$body_template = trans('texts.invoice_message', [], null, $this->client->locale());
}
$subject_template = $client->getSetting('email_subject_'.$reminder_template);
if(iconv_strlen($subject_template) == 0){
if($reminder_template == 'invoice')
$subject_template = trans('texts.invoice_subject', [], null, $this->client->locale());
else
$subject_template = trans('texts.reminder_subject', [], null, $this->client->locale());
}
$data['body'] = $this->parseTemplate($body_template, false);
$data['subject'] = $this->parseTemplate($subject_template, true);

View File

@ -0,0 +1,165 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils\Traits;
use Illuminate\Support\Carbon;
/**
* Class MakesReminders.
*/
trait MakesReminders
{
public function setReminder($settings = null)
{
if(!$settings)
$settings = $this->client->getMergedSettings();
if(!$this->isPayable())
{
$this->next_send_date = null;
$this->save();
return; //exit early
}
$nsd = null;
if($settings->enable_reminder1 !== false &&
$settings->schedule_reminder1 == 'after_invoice_date' &&
$settings->num_days_reminder1 > 0)
{
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder1);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder1 !== false &&
$settings->schedule_reminder1 == 'before_due_date' &&
$settings->num_days_reminder1 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder1);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder1 !== false &&
$settings->schedule_reminder1 == 'after_due_date' &&
$settings->num_days_reminder1 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder1);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder2 !== false &&
$settings->schedule_reminder2 == 'after_invoice_date' &&
$settings->num_days_reminder2 > 0)
{
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder2);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder2 !== false &&
$settings->schedule_reminder2 == 'before_due_date' &&
$settings->num_days_reminder2 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder2);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder2 !== false &&
$settings->schedule_reminder2 == 'after_due_date' &&
$settings->num_days_reminder2 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder2);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder3 !== false &&
$settings->schedule_reminder3 == 'after_invoice_date' &&
$settings->num_days_reminder3 > 0)
{
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder3);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder3 !== false &&
$settings->schedule_reminder3 == 'before_due_date' &&
$settings->num_days_reminder3 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder3);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
if($settings->enable_reminder3 !== false &&
$settings->schedule_reminder3 == 'after_due_date' &&
$settings->num_days_reminder3 > 0)
{
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder3);
if(!$nsd)
$nsd = $reminder_date;
if($reminder_date->lt($nsd))
$nsd = $reminder_date;
}
$this->next_send_date = $nsd;
$this->save();
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace Tests\Integration;
use App\Models\Invoice;
use App\Utils\Traits\MakesReminders;
use Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Utils\Traits\MakesReminder
*/
class CheckRemindersTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
use MakesReminders;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
}
public function test_after_invoice_date_reminder()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$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->markSent();
$this->invoice->setReminder($settings);
$this->assertEquals(0, Carbon::now()->addDays(7)->diffInDays($this->invoice->next_send_date));
}
public function test_no_reminders_sent_to_paid_invoices()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$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->markSent();
$this->invoice->setStatus(Invoice::STATUS_PAID);
$this->invoice->setReminder($settings);
$this->assertEquals($this->invoice->next_send_date, null);
}
public function test_before_due_date_reminder()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$settings = $this->company->settings;
$settings->enable_reminder1 = true;
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->num_days_reminder1 = 50;
$settings->enable_reminder2 = true;
$settings->schedule_reminder2 = 'before_due_date';
$settings->num_days_reminder2 = 29;
$settings->enable_reminder3 = true;
$settings->schedule_reminder3 = 'after_due_date';
$settings->num_days_reminder3 = 1;
$this->company->settings = $settings;
$this->invoice->markSent();
$this->invoice->setReminder($settings);
$this->assertEquals(0, Carbon::parse($this->invoice->due_date)->subDays(29)->diffInDays($this->invoice->next_send_date));
}
public function test_after_due_date_reminder()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$settings = $this->company->settings;
$settings->enable_reminder1 = true;
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->num_days_reminder1 = 50;
$settings->enable_reminder2 = false;
$settings->schedule_reminder2 = 'before_due_date';
$settings->num_days_reminder2 = 50;
$settings->enable_reminder3 = true;
$settings->schedule_reminder3 = 'after_due_date';
$settings->num_days_reminder3 = 1;
$this->company->settings = $settings;
$this->invoice->markSent();
$this->invoice->setReminder($settings);
$this->assertEquals(0, Carbon::parse($this->invoice->due_date)->addDays(1)->diffInDays($this->invoice->next_send_date));
}
public function test_turning_off_reminders()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$settings = $this->company->settings;
$settings->enable_reminder1 = false;
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->num_days_reminder1 = 50;
$settings->enable_reminder2 = false;
$settings->schedule_reminder2 = 'before_due_date';
$settings->num_days_reminder2 = 50;
$settings->enable_reminder3 = false;
$settings->schedule_reminder3 = 'after_due_date';
$settings->num_days_reminder3 = 1;
$this->company->settings = $settings;
$this->invoice->markSent();
$this->invoice->setReminder($settings);
$this->assertEquals($this->invoice->next_send_date, null);
}
public function test_edge_case_num_days_equals_zero_reminders()
{
$this->invoice->date = now();
$this->invoice->due_date = Carbon::now()->addDays(30);
$settings = $this->company->settings;
$settings->enable_reminder1 = false;
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->num_days_reminder1 = 0;
$settings->enable_reminder2 = false;
$settings->schedule_reminder2 = 'before_due_date';
$settings->num_days_reminder2 = 0;
$settings->enable_reminder3 = false;
$settings->schedule_reminder3 = 'after_due_date';
$settings->num_days_reminder3 = 0;
$this->company->settings = $settings;
$this->invoice->markSent();
$this->invoice->setReminder($settings);
$this->assertEquals($this->invoice->next_send_date, null);
}
}