1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 00:11:35 +02:00

Fixes and Refactors for Invoice Emails. (#3339)

* Working on emailing invoices

* Working on emailing and displaying email

* Working on emailing and displaying email

* Email invoices

* Fixes for html emails

* Ensure valid client prior to store

* Ensure client exists when storing an entity

* Update variable name send -> send_email for client_contacts

* Mailable download files

* Extend timeouts of password protected routes when a protected route is hit

* Add default portal design to company settings

* Minor fixes

* Fixes for Tests

* Fixes for invoicing emails

* Refactors for InvoiceEmail

* Implement abstractservice

* Refactors for services

* Refactors for emails

* Fixes for Invoice Emails
This commit is contained in:
David Bomba 2020-02-17 20:37:44 +11:00 committed by GitHub
parent c148157bac
commit f57339f185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 379 additions and 180 deletions

View File

@ -2,10 +2,13 @@
namespace App\Console\Commands;
use App\DataMapper\DefaultSettings;
use App\Factory\ClientFactory;
use App\Factory\CompanyUserFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceInvitationFactory;
use App\Helpers\Email\InvoiceEmail;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Mail\TemplateEmail;
use App\Models\Client;
use App\Models\ClientContact;
@ -49,9 +52,7 @@ class SendTestEmails extends Command
public function handle()
{
$this->sendTemplateEmails('plain');
sleep(5);
$this->sendTemplateEmails('light');
sleep(5);
$this->sendTemplateEmails('dark');
}
@ -68,24 +69,46 @@ class SendTestEmails extends Command
$user = User::whereEmail('user@example.com')->first();
$account = factory(\App\Models\Account::class)->create();
$company = factory(\App\Models\Company::class)->create([
'account_id' => $account->id,
]);
$client = Client::all()->first();
if (!$user) {
$user = factory(\App\Models\User::class)->create([
'confirmation_code' => '123',
'email' => $faker->safeEmail,
'first_name' => 'John',
'last_name' => 'Doe',
]);
$account = factory(\App\Models\Account::class)->create();
$company = factory(\App\Models\Company::class)->create([
'account_id' => $account->id,
]);
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'permissions' => '',
'settings' => DefaultSettings::userSettings(),
]);
}
else
{
$company = $user->company_users->first()->company;
$account = $company->account;
}
$client = Client::all()->first();
if (!$client) {
$client = ClientFactory::create($company->id, $user->id);
$client->save();
@ -118,13 +141,18 @@ class SendTestEmails extends Command
$ii->save();
$invoice->setRelation('invitations', $ii);
$invoice->service()->markSent()->save();
$invoice->save();
CreateInvoicePdf::dispatch($invoice, $company, $client->primary_contact()->first());
$cc_emails = [config('ninja.testvars.test_email')];
$bcc_emails = [config('ninja.testvars.test_email')];
$email_builder = (new InvoiceEmail())->build($invoice, null, $client->primary_contact()->first());
$email_builder = (new InvoiceEmail())->build($ii, 'invoice');
$email_builder->setFooter($message['footer'])
->setSubject($message['subject'])
->setBody($message['body']);
Mail::to(config('ninja.testvars.test_email'), 'Mr Test')
->cc($cc_emails)

View File

@ -33,6 +33,8 @@ class CompanySettings extends BaseSettings {
public $document_email_attachment = false;
public $send_portal_password = false;
public $portal_design_id = '1';
public $timezone_id = '';
public $date_format_id = '';
public $military_time = false;
@ -218,6 +220,7 @@ class CompanySettings extends BaseSettings {
public $invoice_variables = [];
public static $casts = [
'portal_design_id' => 'string',
'late_fee_endless_percent' => 'float',
'late_fee_endless_amount' => 'float',
'auto_email_invoice' => 'bool',

View File

@ -1,15 +1,13 @@
<?php
namespace App\Helpers\Email;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Quote;
use League\CommonMark\CommonMarkConverter;
abstract class EmailBuilder
class EmailBuilder
{
protected $subject;
protected $body;
@ -84,7 +82,7 @@ abstract class EmailBuilder
*/
public function setBody($body)
{
$this->parseTemplate($body, true);
$this->body = $this->parseTemplate($body, true);
return $this;
}

View File

@ -10,23 +10,27 @@ namespace App\Helpers\Email;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
class InvoiceEmail extends EmailBuilder
{
public function build(Invoice $invoice, $reminder_template, $contact = null)
public function build(InvoiceInvitation $invitation, $reminder_template)
{
$client = $invoice->client;
$client = $invitation->contact->client;
$invoice = $invitation->invoice;
$contact = $invitation->contact;
if(!$reminder_template)
$reminder_template = $invoice->calculateTemplate();
$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',
['amount' => $invoice->present()->amount(), 'company' => $invoice->company->present()->name()], null,
['invoice' => $invoice->number, 'company' => $invoice->company->present()->name()], null,
$invoice->client->locale());
}
@ -36,26 +40,26 @@ class InvoiceEmail extends EmailBuilder
if ($reminder_template == 'quote') {
$subject_template = trans('texts.invoice_subject',
[
'number' => $invoice->present()->invoice_number(),
'company' => $invoice->company->present()->name()
'invoice' => $invoice->present()->invoice_number(),
'account' => $invoice->company->present()->name()
],
null, $invoice->client->locale());
} else {
$subject_template = trans('texts.reminder_subject',
[
'number' => $invoice->present()->invoice_number(),
'company' => $invoice->company->present()->name()
'invoice' => $invoice->present()->invoice_number(),
'account' => $invoice->company->present()->name()
],
null, $invoice->client->locale());
}
}
$this->setTemplate($invoice->client->getSetting('email_style'))
->setContact($contact)
->setVariables($invoice->makeValues($contact))
->setSubject($subject_template)
->setBody($body_template)
->setFooter("<a href='{$invoice->invitations->first()->getLink()}'>Invoice Link</a>");
->setFooter("<a href='{$invitation->getLink()}'>{$invitation->getLink()}</a>");
if ($client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments($invoice->pdf_file_path());

View File

@ -9,15 +9,19 @@
namespace App\Helpers\Email;
use App\Models\Quote;
use App\Models\QuoteInvitation;
class QuoteEmail extends EmailBuilder
{
public function build(Quote $quote, $reminder_template, $contact = null)
public function build(QuoteInvitation $invitation, $reminder_template)
{
$client = $quote->client;
$this->template_style = $quote->client->getSetting('email_style');
$client = $invitation->client;
$quote = $invitation->quote;
$contact = $invitation->contact;
$this->template_style = $client->getSetting('email_style');
$body_template = $client->getSetting('email_template_' . $reminder_template);
@ -44,7 +48,7 @@ class QuoteEmail extends EmailBuilder
$this->setTemplate($quote->client->getSetting('email_style'))
->setContact($contact)
->setFooter("<a href='{$quote->invitations->first()->getLink()}'>Invoice Link</a>")
->setFooter("<a href='{$invitation->getLink()}'>Quote Link</a>")
->setVariables($quote->makeValues($contact))
->setSubject($subject_template)
->setBody($body_template);

View File

@ -666,7 +666,9 @@ class InvoiceController extends BaseController {
case 'email':
$invoice->invitations->each(function ($invitation) use($invoice){
EmailInvoice::dispatch((new InvoiceEmail)->build($invoice, null, null), $invitation, $invoice->company);
$email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template);
EmailInvoice::dispatch($email_builder, $invitation, $invoice->company);
});

View File

@ -6,7 +6,6 @@
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"),
* @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="company_id", type="string", example="", description="________"),
* @OA\Property(property="client_id", type="string", example="", description="________"),
* @OA\Property(
* property="contacts",
* type="array",

View File

@ -36,7 +36,7 @@ class Cors
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range');
$response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range');
return $response;
}

View File

@ -40,6 +40,10 @@ class PasswordProtection
return response()->json($error, 403);
}
} elseif (Cache::get(auth()->user()->email."_logged_in")) {
Cache::pull(auth()->user()->email."_logged_in");
Cache::add(auth()->user()->email."_logged_in", Str::random(64), now()->addMinutes(10));
return $next($request);
} else {
$error = [

View File

@ -40,7 +40,9 @@ class StoreCreditRequest extends FormRequest
{
$input = $this->all();
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
if($input['client_id'])
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
$this->replace($input);

View File

@ -46,7 +46,9 @@ class StoreInvoiceRequest extends Request
{
$input = $this->all();
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
if($input['client_id'])
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
$this->replace($input);

View File

@ -45,7 +45,9 @@ class StoreRecurringInvoiceRequest extends Request
{
$input = $this->all();
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
if($input['client_id'])
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
$this->replace($input);

View File

@ -45,7 +45,9 @@ class StoreRecurringQuoteRequest extends Request
{
$input = $this->all();
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
if($input['client_id'])
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
//$input['line_items'] = json_encode($input['line_items']);
$this->replace($input);

View File

@ -12,6 +12,7 @@
namespace App\Jobs\Invoice;
use App\Libraries\MultiDB;
use App\Mail\DownloadInvoices;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
@ -19,15 +20,16 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Mail;
class ZipInvoices implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice;
public $invoices;
private $company;
@ -66,7 +68,7 @@ class ZipInvoices implements ShouldQueue
$zip = new ZipStream($file_name, $options);
foreach ($invoices as $invoice) {
foreach ($this->invoices as $invoice) {
$zip->addFileFromPath(basename($invoice->pdf_file_path()), public_path($invoice->pdf_file_path()));
}
@ -76,9 +78,9 @@ class ZipInvoices implements ShouldQueue
fclose($tempStream);
//fire email here
return Storage::disk(config('filesystems.default'))->url($path . $file_name);
Mail::to(config('ninja.contact.ninja_official_contact'))
->send(new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path . $file_name)));
}
}

View File

@ -38,6 +38,7 @@ class CreateInvoiceHtmlBackup implements ShouldQueue
*/
public function handle($event)
{
MultiDB::setDB($event->company->db);
$fields = new \stdClass;

View File

@ -0,0 +1,36 @@
<?php
namespace App\Mail;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class DownloadInvoices extends Mailable
{
use Queueable, SerializesModels;
public $file_path;
public function __construct($file_path)
{
$this->file_path = $file_path;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->from(config('mail.from.address')) //todo this needs to be fixed to handle the hosted version
->subject(ctrans('texts.download_documents',['size'=>'']))
->markdown('email.admin.download_files', [
'file_path' => $this->file_path
]);
}
}

View File

@ -56,7 +56,7 @@ class Client extends BaseModel implements HasLocalePreference
'user_id',
'company_id',
'backup',
'settings',
// 'settings',
'last_login',
'private_notes'
];
@ -245,6 +245,7 @@ class Client extends BaseModel implements HasLocalePreference
*/
public function getMergedSettings() :object
{
if ($this->group_settings !== null) {
$group_settings = ClientSettings::buildClientSettings($this->group_settings->settings, $this->settings);
@ -431,7 +432,7 @@ class Client extends BaseModel implements HasLocalePreference
$languages = Cache::get('languages');
return $languages->filter(function ($item) {
return $item->id == $this->client->getSetting('language_id');
return $item->id == $this->getSetting('language_id');
})->first()->locale;
//$lang = Language::find($this->client->getSetting('language_id'));

View File

@ -0,0 +1,19 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Services;
abstract class AbstractService
{
abstract public function run();
}

View File

@ -5,34 +5,36 @@ namespace App\Services\Credit;
use App\Credit;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Jobs\Customer\UpdateCustomerBalance;
use App\Jobs\Customer\UpdateCustomerPaidToDate;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Customer\CustomerService;
use App\Services\Payment\PaymentService;
use App\Traits\GeneratesCounter;
class ApplyNumber
class ApplyNumber extends AbstractService
{
use GeneratesCounter;
private $customer;
public function __construct($customer)
public function __construct(Client $client)
{
$this->customer = $customer;
$this->client = $client;
}
public function __invoke($credit)
public function run($credit)
{
if ($credit->number != '') {
return $credit;
}
$credit->number = $this->getNextCreditNumber($this->customer);
$credit->number = $this->getNextCreditNumber($this->client);
return $credit;

View File

@ -3,18 +3,19 @@ namespace App\Services\Credit;
use App\Factory\CreditInvitationFactory;
use App\Models\CreditInvitation;
use App\Services\AbstractService;
class CreateInvitations
class CreateInvitations extends AbstractService
{
public function __construct()
{
}
public function __invoke($credit)
public function run($credit)
{
$contacts = $credit->customer->contacts;
$contacts = $credit->client->contacts;
$contacts->each(function ($contact) use($credit){
$invitation = CreditInvitation::whereCompanyId($credit->account_id)
@ -23,11 +24,11 @@ class CreateInvitations
->first();
if (!$invitation) {
$ii = CreditInvitationFactory::create($credit->account_id, $credit->user_id);
$ii = CreditInvitationFactory::create($credit->company_id, $credit->user_id);
$ii->credit_id = $credit->id;
$ii->client_contact_id = $contact->id;
$ii->save();
} elseif ($invitation && !$contact->send_credit) {
} elseif ($invitation && !$contact->send_email) {
$invitation->delete();
}
});

View File

@ -3,9 +3,10 @@
namespace App\Services\Credit;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Services\AbstractService;
use Illuminate\Support\Facades\Storage;
class GetCreditPdf
class GetCreditPdf extends AbstractService
{
public function __construct()
@ -15,16 +16,16 @@ class GetCreditPdf
public function __invoke($credit, $contact = null)
{
if (!$contact) {
$contact = $credit->customer->primary_contact()->first();
$contact = $credit->client->primary_contact()->first();
}
$path = 'public/' . $credit->customer->id . '/credits/';
$path = 'public/' . $credit->client->id . '/credits/';
$file_path = $path . $credit->number . '.pdf';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
if (!$file) {
$file_path = CreateInvoicePdf::dispatchNow($this, $credit->account, $contact);
$file_path = CreateInvoicePdf::dispatchNow($this, $credit->company, $contact);
}
return Storage::disk($disk)->url($file_path);

View File

@ -14,35 +14,41 @@ namespace App\Services\Invoice;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
use App\Services\Payment\PaymentService;
use App\Utils\Traits\GeneratesCounter;
class ApplyNumber
class ApplyNumber extends AbstractService
{
use GeneratesCounter;
private $client;
public function __construct($client)
private $invoice;
public function __construct(Client $client, Invoice $invoice)
{
$this->client = $client;
$this->invoice = $invoice;
}
public function run($invoice)
public function run()
{
if ($invoice->number != '')
return $invoice;
if ($this->invoice->number != '')
return $this->invoice;
switch ($this->client->getSetting('counter_number_applied')) {
case 'when_saved':
$invoice->number = $this->getNextInvoiceNumber($this->client);
$this->invoice->number = $this->getNextInvoiceNumber($this->client);
break;
case 'when_sent':
if ($invoice->status_id == Invoice::STATUS_SENT) {
$invoice->number = $this->getNextInvoiceNumber($this->client);
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
$this->invoice->number = $this->getNextInvoiceNumber($this->client);
}
break;
@ -51,6 +57,6 @@ class ApplyNumber
break;
}
return $invoice;
return $this->invoice;
}
}

View File

@ -14,46 +14,53 @@ namespace App\Services\Invoice;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
class ApplyPayment
class ApplyPayment extends AbstractService
{
private $invoice;
public function __construct($invoice)
private $payment;
private $payment_amount;
public function __construct($invoice, $payment, $payment_amount)
{
$this->invoice = $invoice;
$this->payment = $payment;
$this->payment_amount = $payment_amount;
}
public function run($payment, $payment_amount)
public function run()
{
UpdateCompanyLedgerWithPayment::dispatchNow($payment, ($payment_amount*-1), $payment->company);
UpdateCompanyLedgerWithPayment::dispatchNow($this->payment, ($this->payment_amount*-1), $this->payment->company);
$payment->client->service()->updateBalance($payment_amount*-1)->save();
$this->payment->client->service()->updateBalance($this->payment_amount*-1)->save();
/* Update Pivot Record amount */
$payment->invoices->each(function ($inv) use($payment_amount){
$this->payment->invoices->each(function ($inv){
if ($inv->id == $this->invoice->id) {
$inv->pivot->amount = $payment_amount;
$inv->pivot->amount = $this->payment_amount;
$inv->pivot->save();
}
});
if ($this->invoice->hasPartial()) {
//is partial and amount is exactly the partial amount
if ($this->invoice->partial == $payment_amount) {
$this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($payment_amount*-1);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial > $payment_amount) { //partial amount exists, but the amount is less than the partial amount
$this->invoice->service()->updatePartial($payment_amount*-1)->updateBalance($payment_amount*-1);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial < $payment_amount) { //partial exists and the amount paid is GREATER than the partial amount
$this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($payment_amount*-1);
if ($this->invoice->partial == $this->payment_amount) {
$this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($this->payment_amount*-1);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial > $this->payment_amount) { //partial amount exists, but the amount is less than the partial amount
$this->invoice->service()->updatePartial($this->payment_amount*-1)->updateBalance($this->payment_amount*-1);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial < $this->payment_amount) { //partial exists and the amount paid is GREATER than the partial amount
$this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($this->payment_amount*-1);
}
} elseif ($payment_amount == $this->invoice->balance) { //total invoice paid.
$this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($payment_amount*-1);
} elseif($payment_amount < $this->invoice->balance) { //partial invoice payment made
$this->invoice->service()->clearPartial()->updateBalance($payment_amount*-1);
} elseif ($this->payment_amount == $this->invoice->balance) { //total invoice paid.
$this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($this->payment_amount*-1);
} elseif($this->payment_amount < $this->invoice->balance) { //partial invoice payment made
$this->invoice->service()->clearPartial()->updateBalance($this->payment_amount*-1);
}
return $this->invoice;

View File

@ -15,29 +15,31 @@ namespace App\Services\Invoice;
use App\Factory\InvoiceInvitationFactory;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Services\AbstractService;
class CreateInvitations
class CreateInvitations extends AbstractService
{
public function __construct()
private $invoice;
public function __construct(Invoice $invoice)
{
// ..
$this->invoice = $invoice;
}
public function run($invoice)
public function run()
{
$contacts = $invoice->client->contacts;
$this->invoice->client->contacts->each(function ($contact){
$contacts->each(function ($contact) use($invoice){
$invitation = InvoiceInvitation::whereCompanyId($invoice->company_id)
$invitation = InvoiceInvitation::whereCompanyId($this->invoice->company_id)
->whereClientContactId($contact->id)
->whereInvoiceId($invoice->id)
->whereInvoiceId($this->invoice->id)
->first();
if (!$invitation && $contact->send) {
$ii = InvoiceInvitationFactory::create($invoice->company_id, $invoice->user_id);
$ii->invoice_id = $invoice->id;
$ii = InvoiceInvitationFactory::create($this->invoice->company_id, $this->invoice->user_id);
$ii->invoice_id = $this->invoice->id;
$ii->client_contact_id = $contact->id;
$ii->save();
} elseif ($invitation && !$contact->send) {
@ -45,6 +47,6 @@ class CreateInvitations
}
});
return $invoice;
return $this->invoice;
}
}

View File

@ -12,20 +12,32 @@
namespace App\Services\Invoice;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Services\AbstractService;
use Illuminate\Support\Facades\Storage;
class GetInvoicePdf
class GetInvoicePdf extends AbstractService
{
public function run($invoice, $contact = null)
public function __construct(Invoice $invoice, ClientContact $contact = null)
{
$this->invoice = $invoice;
$this->contact = $contact;
}
public function run()
{
if(!$contact)
$contact = $invoice->client->primary_contact()->first();
if(!$this->contact)
$this->contact = $this->invoice->client->primary_contact()->first();
$path = $invoice->client->invoice_filepath();
$path = $this->invoice->client->invoice_filepath();
$file_path = $path . $invoice->number . '.pdf';
$file_path = $path . $this->invoice->number . '.pdf';
$disk = config('filesystems.default');
@ -34,7 +46,7 @@ class GetInvoicePdf
if(!$file)
{
$file_path = CreateInvoicePdf::dispatchNow($invoice, $invoice->company, $contact);
$file_path = CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->contact);
}

View File

@ -44,9 +44,9 @@ class InvoiceService
*/
public function markPaid()
{
$mark_invoice_paid = new MarkPaid($this->client_service);
$mark_invoice_paid = new MarkPaid($this->client_service, $this->invoice);
$this->invoice = $mark_invoice_paid->run($this->invoice);
$this->invoice = $mark_invoice_paid->run();
return $this;
}
@ -57,9 +57,9 @@ class InvoiceService
*/
public function applyNumber()
{
$apply_number = new ApplyNumber($this->invoice->client);
$apply_number = new ApplyNumber($this->invoice->client, $this->invoice);
$this->invoice = $apply_number->run($this->invoice);
$this->invoice = $apply_number->run();
return $this;
}
@ -72,9 +72,9 @@ class InvoiceService
*/
public function applyPayment(Payment $payment, float $payment_amount)
{
$apply_payment = new ApplyPayment($this->invoice);
$apply_payment = new ApplyPayment($this->invoice, $payment, $payment_amount);
$this->invoice = $apply_payment->run($payment, $payment_amount);
$this->invoice = $apply_payment->run();
return $this;
}
@ -88,27 +88,27 @@ class InvoiceService
*/
public function updateBalance($balance_adjustment)
{
$update_balance = new UpdateBalance($this->invoice);
$update_balance = new UpdateBalance($this->invoice, $balance_adjustment);
$this->invoice = $update_balance->run($balance_adjustment);
$this->invoice = $update_balance->run();
return $this;
}
public function createInvitations()
{
$create_invitation = new CreateInvitations();
$create_invitation = new CreateInvitations($this->invoice);
$this->invoice = $create_invitation->run($this->invoice);
$this->invoice = $create_invitation->run();
return $this;
}
public function markSent()
{
$mark_sent = new MarkSent($this->invoice->client);
{
$mark_sent = new MarkSent($this->invoice->client, $this->invoice);
$this->invoice = $mark_sent->run($this->invoice);
$this->invoice = $mark_sent->run();
return $this;
}
@ -116,16 +116,16 @@ class InvoiceService
public function getInvoicePdf($contact)
{
$get_invoice_pdf = new GetInvoicePdf();
$get_invoice_pdf = new GetInvoicePdf($this->invoice, $contact);
return $get_invoice_pdf->run($this->invoice, $contact);
return $get_invoice_pdf->run();
}
public function sendEmail($contact)
{
$send_email = new SendEmail($this->invoice);
$send_email = new SendEmail($this->invoice, null, $contact);
return $send_email->run(null, $contact);
return $send_email->run();
}
public function markViewed()

View File

@ -16,39 +16,44 @@ use App\Factory\PaymentFactory;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
use App\Services\Payment\PaymentService;
class MarkPaid
class MarkPaid extends AbstractService
{
private $client_service;
public function __construct($client_service)
private $invoice;
public function __construct(ClientService $client_service, Invoice $invoice)
{
$this->client_service = $client_service;
$this->invoice = $invoice;
}
public function run($invoice)
public function run()
{
if($invoice->status_id == Invoice::STATUS_DRAFT)
$invoice->service()->markSent();
if($this->invoice->status_id == Invoice::STATUS_DRAFT)
$this->invoice->service()->markSent();
/* Create Payment */
$payment = PaymentFactory::create($invoice->company_id, $invoice->user_id);
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $invoice->balance;
$payment->amount = $this->invoice->balance;
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $invoice->client_id;
$payment->client_id = $this->invoice->client_id;
$payment->transaction_reference = ctrans('texts.manual_entry');
/* Create a payment relationship to the invoice entity */
$payment->save();
$payment->invoices()->attach($invoice->id, [
$payment->invoices()->attach($this->invoice->id, [
'amount' => $payment->amount
]);
$invoice->service()
$this->invoice->service()
->updateBalance($payment->amount*-1)
->setStatus(Invoice::STATUS_PAID)
->save();
@ -63,7 +68,7 @@ class MarkPaid
->updatePaidToDate($payment->amount)
->save();
return $invoice;
return $this->invoice;
}
}

View File

@ -14,38 +14,42 @@ namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasMarkedSent;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Models\Invoice;
use App\Services\AbstractService;
class MarkSent
class MarkSent extends AbstractService
{
private $client;
public function __construct($client)
private $invoice;
public function __construct($client, $invoice)
{
$this->client = $client;
$this->invoice = $invoice;
}
public function run($invoice)
public function run()
{
/* Return immediately if status is not draft */
if ($invoice->status_id != Invoice::STATUS_DRAFT) {
return $invoice;
if ($this->invoice->status_id != Invoice::STATUS_DRAFT) {
return $this->invoice;
}
$invoice->markInvitationsSent();
$this->invoice->markInvitationsSent();
$invoice->setReminder();
$this->invoice->setReminder();
event(new InvoiceWasMarkedSent($invoice, $invoice->company));
event(new InvoiceWasMarkedSent($this->invoice, $this->invoice->company));
$this->client->service()->updateBalance($invoice->balance)->save();
$this->client->service()->updateBalance($this->invoice->balance)->save();
$invoice->service()->setStatus(Invoice::STATUS_SENT)->applyNumber()->save();
$this->invoice->service()->setStatus(Invoice::STATUS_SENT)->applyNumber()->save();
UpdateCompanyLedgerWithInvoice::dispatchNow($invoice, $invoice->balance, $invoice->company);
UpdateCompanyLedgerWithInvoice::dispatchNow($this->invoice, $this->invoice->balance, $this->invoice->company);
return $invoice;
return $this->invoice;
}
}

View File

@ -13,17 +13,23 @@ namespace App\Services\Invoice;
use App\Helpers\Email\InvoiceEmail;
use App\Jobs\Invoice\EmailInvoice;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Services\AbstractService;
use Illuminate\Support\Carbon;
class SendEmail
class SendEmail extends AbstractService
{
protected $invoice;
public function __construct(Invoice $invoice)
public function __construct(Invoice $invoice, $reminder_template = null, ClientContact $contact = null)
{
$this->invoice = $invoice;
$this->reminder_template = $reminder_template;
$this->contact = $contact;
}
@ -32,15 +38,17 @@ class SendEmail
* @param string $reminder_template The template name ie reminder1
* @return array
*/
public function run($reminder_template = null, $contact = null): array
public function run()
{
if (!$reminder_template) {
$reminder_template = $this->invoice->status_id == Invoice::STATUS_DRAFT || Carbon::parse($this->invoice->due_date) > now() ? 'invoice' : $this->invoice->calculateTemplate();
if (!$this->reminder_template) {
$this->reminder_template = $this->invoice->calculateTemplate();
}
$email_builder = (new InvoiceEmail())->build($this->invoice, $reminder_template, $contact);
$this->invoice->invitations->each(function ($invitation){
$email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template);
$this->invoice->invitations->each(function ($invitation) use ($email_builder) {
if ($invitation->contact->send && $invitation->contact->email) {
EmailInvoice::dispatch($email_builder, $invitation, $invitation->company);
}

View File

@ -12,31 +12,33 @@
namespace App\Services\Invoice;
use App\Models\Invoice;
use App\Services\AbstractService;
class UpdateBalance
class UpdateBalance extends AbstractService
{
private $invoice;
public function __construct($invoice)
private $balance_adjustment;
public function __construct($invoice, $balance_adjustment)
{
$this->invoice = $invoice;
$this->balance_adjustment = $balance_adjustment;
}
public function run($balance_adjustment)
public function run()
{
if ($this->invoice->is_deleted) {
return;
}
$balance_adjustment = floatval($balance_adjustment);
$this->invoice->balance += $balance_adjustment;
$this->invoice->balance += floatval($this->balance_adjustment);
if ($this->invoice->balance == 0) {
$this->status_id = Invoice::STATUS_PAID;
$this->invoice->status_id = Invoice::STATUS_PAID;
// $this->save();
// event(new InvoiceWasPaid($this, $this->company));

View File

@ -27,10 +27,13 @@ class SendEmail
$reminder_template = $this->quote->status_id == Quote::STATUS_DRAFT || Carbon::parse($this->quote->due_date) > now() ? 'invoice' : $this->quote->calculateTemplate();
}
$email_builder = (new QuoteEmail())->build($this->quote, $reminder_template, $contact);
$this->quote->invitations->each(function ($invitation){
if ($invitation->contact->send && $invitation->contact->email)
{
$email_builder = (new QuoteEmail())->build($invitation, $reminder_template);
$this->quote->invitations->each(function ($invitation) use ($email_builder) {
if ($invitation->contact->send && $invitation->contact->email) {
EmailQuote::dispatchNow($email_builder, $invitation);
}
});

View File

@ -34,10 +34,11 @@ trait MakesInvoiceHtml
{
//$variables = array_merge($invoice->makeLabels(), $invoice->makeValues());
//$design = str_replace(array_keys($variables), array_values($variables), $design);
if(!$contact)
$contact = $invoice->client->primary_contact()->first();
$invoice->load('client');
App::setLocale($contact->preferredLocale());
$client = $invoice->client;
App::setLocale($client->preferredLocale());
$labels = $invoice->makeLabels();
$values = $invoice->makeValues($contact);

View File

@ -576,8 +576,11 @@ trait MakesInvoiceValues
* @param array $items The array of invoice items
* @return array The formatted array of invoice items
*/
private function transformLineItems(array $items) :array
private function transformLineItems($items) :array
{
if(!is_array($items))
return [];
foreach ($items as $item) {
$item->cost = Number::formatMoney($item->cost, $this->client);
$item->line_total = Number::formatMoney($item->line_total, $this->client);

View File

@ -378,7 +378,7 @@ class CreateUsersTable extends Migration
$table->string('password');
$table->string('token')->nullable();
$table->boolean('is_locked')->default(false);
$table->boolean('send')->default(true);
$table->boolean('send_email')->default(true);
$table->string('contact_key')->nullable();
$table->rememberToken();
$table->timestamps(6);

View File

@ -0,0 +1,20 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
Download
@endcomponent
@endslot
{{-- Body --}}
{{ $file_path }}
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('ninja.app_name') }}.
@endcomponent
@endslot
@endcomponent

View File

@ -1,13 +1,12 @@
<!DOCTYPE html>
<html lang="{{ App::getLocale() }}">
<head>
<meta charset="utf-8">
<meta charset="utf-8">
</head>
<body>
{!! $body !!}
{!! $footer !!}
{!! $body !!}
</body>
<footer>
{!! $footer !!}
</footer>
</html>

View File

@ -87,7 +87,7 @@
<div>
<h2>trans('texts.sign_up_now')</h2>
<p>trans('texts.not_a_member_yet')</p>
<a class="btn btn-primary active mt-3" href="{{route('signup') }}">trans('texts.login_create_an_account')</a>
<a class="btn btn-primary active mt-3" href=" route('signup') ">trans('texts.login_create_an_account')</a>
</div>
</div>
</div>

View File

@ -41,7 +41,7 @@ class InvoiceEmailTest extends TestCase
$this->makeTestData();
}
public function test_initial_email_sends()
public function test_initial_email_send_emails()
{
$this->invoice->date = now();
@ -50,15 +50,14 @@ class InvoiceEmailTest extends TestCase
$this->invoice->client_id = $this->client->id;
$this->invoice->setRelation('client', $this->client);
$this->invoice->save();
$invitations = InvoiceInvitation::whereInvoiceId($this->invoice->id)->get();
$this->invoice->invitations->each(function ($invitation) {
$email_builder = (new InvoiceEmail())->build($this->invoice, null, null);
if ($invitation->contact->send_email && $invitation->contact->email) {
$invitations->each(function ($invitation) use ($email_builder) {
if ($invitation->contact->send && $invitation->contact->email) {
$email_builder = (new InvoiceEmail())->build($invitation, null);
EmailInvoice::dispatch($email_builder, $invitation, $invitation->company);

View File

@ -364,8 +364,18 @@ class PaymentTest extends TestCase
$this->invoice = null;
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->setRelation('company', $this->company);
$client->save();
$client_contact = factory(\App\Models\ClientContact::class, 1)->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1
]);
$client->setRelation('contacts', $client_contact);
$this->invoice = InvoiceFactory::create($this->company->id,$this->user->id);//stub the company and user_id
$this->invoice->client_id = $client->id;
@ -379,6 +389,8 @@ class PaymentTest extends TestCase
$this->invoice_calc->build();
$this->invoice = $this->invoice_calc->getInvoice();
$this->invoice->company->setRelation('company', $this->company);
$this->invoice->company->setRelation('client', $client);
$this->invoice->save();
$this->invoice->service()->markSent()->save();
$this->invoice->is_deleted = false;

View File

@ -139,14 +139,14 @@ trait MockAccountData
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send' => true,
'send_email' => true,
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'send' => true
'send_email' => true
]);
@ -176,6 +176,9 @@ trait MockAccountData
$this->invoice = $this->invoice_calc->getInvoice();
$this->invoice->setRelation('client', $this->client);
$this->invoice->setRelation('company', $this->company);
$this->invoice->save();
$this->invoice->service()->markSent();
@ -200,13 +203,13 @@ trait MockAccountData
->whereInvoiceId($this->invoice->id)
->first();
if(!$invitation && $contact->send) {
if(!$invitation && $contact->send_email) {
$ii = InvoiceInvitationFactory::create($this->invoice->company_id, $this->invoice->user_id);
$ii->invoice_id = $this->invoice->id;
$ii->client_contact_id = $contact->id;
$ii->save();
}
else if($invitation && !$contact->send) {
else if($invitation && !$contact->send_email) {
$invitation->delete();
}