Merge pull request #4938 from turbo124/v5-develop

Refactoring emails
This commit is contained in:
David Bomba 2021-02-18 14:47:40 +11:00 committed by GitHub
commit dd72740534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 878 additions and 290 deletions

View File

@ -39,10 +39,10 @@ class PaymentWasEmailedAndFailed
* PaymentWasEmailedAndFailed constructor.
* @param Payment $payment
* @param $company
* @param array $errors
* @param string $errors
* @param array $event_vars
*/
public function __construct(Payment $payment, Company $company, array $errors, array $event_vars)
public function __construct(Payment $payment, Company $company, string $errors, array $event_vars)
{
$this->payment = $payment;

View File

@ -532,7 +532,7 @@ class InvoiceController extends BaseController
}
});
ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user()->email);
ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200);
}

View File

@ -524,7 +524,7 @@ class QuoteController extends BaseController
}
});
ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user()->email);
ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200);
}

View File

@ -369,7 +369,6 @@ class UserController extends BaseController
*/
public function update(UpdateUserRequest $request, User $user)
{
$old_email = $user->email;
$old_company_user = $user->company_user;
$old_user = $user;
@ -378,9 +377,8 @@ class UserController extends BaseController
$user = $this->user_repo->save($request->all(), $user);
$user = $user->fresh();
if ($old_email != $new_email) {
UserEmailChanged::dispatch($new_email, $old_email, auth()->user()->company());
}
if ($old_user->email != $new_email)
UserEmailChanged::dispatch($new_user, $old_user, auth()->user()->company());
if(
strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 ||

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -0,0 +1,78 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return bool|Item
*/
public function transform($data)
{
if (isset($data->name) && $this->hasClient($data->name)) {
return false;
}
$settings = new \stdClass;
$settings->currency_id = (string)$this->getCurrencyByCode($data);
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'city' => $this->getString($data, 'client.city'),
'state' => $this->getString($data, 'client.state'),
'shipping_address1' => $this->getString($data, 'client.shipping_address1'),
'shipping_address2' => $this->getString($data, 'client.shipping_address2'),
'shipping_city' => $this->getString($data, 'client.shipping_city'),
'shipping_state' => $this->getString($data, 'client.shipping_state'),
'shipping_postal_code' => $this->getString($data, 'client.shipping_postal_code'),
'public_notes' => $this->getString($data, 'client.public_notes'),
'private_notes' => $this->getString($data, 'client.private_notes'),
'website' => $this->getString($data, 'client.website'),
'vat_number' => $this->getString($data, 'client.vat_number'),
'id_number' => $this->getString($data, 'client.id_number'),
'custom_value1' => $this->getString($data, 'client.custom1'),
'custom_value2' => $this->getString($data, 'client.custom2'),
'custom_value3' => $this->getString($data, 'client.custom3'),
'custom_value4' => $this->getString($data, 'client.custom4'),
'balance' => $this->getFloat($data, 'client.balance'),
'paid_to_date' => $this->getFloat($data, 'client.paid_to_date'),
'credit_balance' => 0,
'settings' => $settings,
'client_hash' => Str::random(40),
'contacts' => [
[
'first_name' => $this->getString($data, 'contact.first_name'),
'last_name' => $this->getString($data, 'contact.last_name'),
'email' => $this->getString($data, 'contact.email'),
'phone' => $this->getString($data, 'contact.phone'),
'custom_value1' => $this->getString($data, 'contact.custom1'),
'custom_value2' => $this->getString($data, 'contact.custom2'),
'custom_value3' => $this->getString($data, 'contact.custom3'),
'custom_value4' => $this->getString($data, 'contact.custom4'),
],
],
'country_id' => isset($data->country_id) ? $this->getCountryId($data->country_id) : null,
'shipping_country_id' => isset($data->shipping_country_id) ? $this->getCountryId($data->shipping_country_id) : null,
];
}
}

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -0,0 +1,46 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers;
/**
* Class InvoiceItemTransformer.
*/
class InvoiceItemTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return bool|Item
*/
public function transform($data)
{
return [
'quantity' => $this->getFloat($data, 'item.quantity'),
'cost' => $this->getFloat($data, 'item.cost'),
'product_key' => $this->getString($data, 'item.product_key'),
'notes' => $this->getString($data, 'item.notes'),
'discount' => $this->getFloat($data, 'item.discount'),
'is_amount_discount' => $this->getString($data, 'item.is_amount_discount'),
'tax_name1' => $this->getString($data, 'item.tax_name1'),
'tax_rate1' => $this->getFloat($data, 'item.tax_rate1'),
'tax_name2' => $this->getString($data, 'item.tax_name2'),
'tax_rate2' => $this->getFloat($data, 'item.tax_rate2'),
'tax_name3' => $this->getString($data, 'item.tax_name3'),
'tax_rate3' => $this->getFloat($data, 'item.tax_rate3'),
'custom_value1' => $this->getString($data, 'item.custom_value1'),
'custom_value2' => $this->getString($data, 'item.custom_value2'),
'custom_value3' => $this->getString($data, 'item.custom_value3'),
'custom_value4' => $this->getString($data, 'item.custom_value4'),
'type_id' => $this->getInvoiceTypeId($data, 'item.type_id'),
];
}
}

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://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return bool|Item
*/
public function transform($data)
{
return [
'company_id' => $this->maps['company']->id,
'number' => $this->getString($data, 'invoice.number'),
'user_id' => $this->getString($data, 'invoice.user_id'),
'amount' => $this->getFloat($data, 'invoice.amount'),
'balance' => $this->getFloat($data, 'invoice.balance'),
'client_id' => $this->getClient($this->getString($data, 'client.name'), $this->getString($data, 'client.email')),
'discount' => $this->getFloat($data, 'invoice.discount'),
'po_number' => $this->getString($data, 'invoice.po_number'),
'date' => $this->getString($data, 'invoice.date'),
'due_date' => $this->getString($data, 'invoice.due_date'),
'terms' => $this->getString($data, 'invoice.terms'),
'public_notes' => $this->getString($data, 'invoice.public_notes'),
'is_sent' => $this->getString($data, 'invoice.is_sent'),
'private_notes' => $this->getString($data, 'invoice.private_notes'),
'tax_name1' => $this->getString($data, 'invoice.tax_name1'),
'tax_rate1' => $this->getFloat($data, 'invoice.tax_rate1'),
'tax_name2' => $this->getString($data, 'invoice.tax_name2'),
'tax_rate2' => $this->getFloat($data, 'invoice.tax_rate2'),
'tax_name3' => $this->getString($data, 'invoice.tax_name3'),
'tax_rate3' => $this->getFloat($data, 'invoice.tax_rate3'),
'custom_value1' => $this->getString($data, 'invoice.custom_value1'),
'custom_value2' => $this->getString($data, 'invoice.custom_value2'),
'custom_value3' => $this->getString($data, 'invoice.custom_value3'),
'custom_value4' => $this->getString($data, 'invoice.custom_value4'),
'footer' => $this->getString($data, 'invoice.footer'),
'partial' => $this->getFloat($data, 'invoice.partial'),
'partial_due_date' => $this->getString($data, 'invoice.partial_due_date'),
'custom_surcharge1' => $this->getString($data, 'invoice.custom_surcharge1'),
'custom_surcharge2' => $this->getString($data, 'invoice.custom_surcharge2'),
'custom_surcharge3' => $this->getString($data, 'invoice.custom_surcharge3'),
'custom_surcharge4' => $this->getString($data, 'invoice.custom_surcharge4'),
'exchange_rate' => $this->getString($data, 'invoice.exchange_rate'),
];
}
}

View File

@ -0,0 +1,46 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers;
/**
* Class PaymentTransformer.
*/
class PaymentTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return bool|Item
*/
public function transform($data)
{
return [
'company_id' => $this->maps['company']->id,
'number' => $this->getString($data, 'payment.number'),
'user_id' => $this->getString($data, 'payment.user_id'),
'amount' => $this->getFloat($data, 'payment.amount'),
'refunded' => $this->getFloat($data, 'payment.refunded'),
'applied' => $this->getFloat($data, 'payment.applied'),
'transaction_reference' => $this->getString($data, 'payment.transaction_reference '),
'date' => $this->getString($data, 'payment.date'),
'private_notes' => $this->getString($data, 'payment.private_notes'),
'number' => $this->getString($data, 'number'),
'custom_value1' => $this->getString($data, 'custom_value1'),
'custom_value2' => $this->getString($data, 'custom_value2'),
'custom_value3' => $this->getString($data, 'custom_value3'),
'custom_value4' => $this->getString($data, 'custom_value4'),
'client_id' => $this->getString($data, 'client_id'),
'invoice_number' => $this->getString($data, 'payment.invoice_number'),
'method' => $this
];
}
}

View File

@ -14,7 +14,6 @@ namespace App\Jobs\Entity;
use App\Events\Invoice\InvoiceReminderWasEmailed;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Jobs\Mail\BaseMailerJob;
use App\Jobs\Mail\EntityFailedSendMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
@ -113,6 +112,7 @@ class EmailEntity implements ShouldQueue
$nmo->entity_string = $this->entity_string;
$nmo->invitation = $this->invitation;
$nmo->reminder_template = $this->reminder_template;
$nmo->entity = $this->entity;
NinjaMailerJob::dispatch($nmo);

View File

@ -96,6 +96,18 @@ class CSVImport implements ShouldQueue {
$this->buildMaps();
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
nlog("starting import");
MultiDB::setDb($this->company->db);
nlog( "import " . $this->import_type );
foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entityType ) {
$csvData = $this->getCsvData( $entityType );
@ -130,6 +142,7 @@ class CSVImport implements ShouldQueue {
MailRouter::dispatch( new ImportCompleted( $data ), $this->company, auth()->user() );
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function preTransformCsv( $csvData, $entityType ) {
if ( empty( $this->column_map[ $entityType ] ) ) {

View File

@ -11,10 +11,12 @@
namespace App\Jobs\Invoice;
use App\Jobs\Mail\BaseMailerJob;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\UnlinkFile;
use App\Mail\DownloadInvoices;
use App\Models\Company;
use App\Models\User;
use App\Utils\TempFile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -26,7 +28,7 @@ use Illuminate\Support\Facades\Storage;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class ZipInvoices extends BaseMailerJob implements ShouldQueue
class ZipInvoices implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -34,7 +36,7 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
private $company;
private $email;
private $user;
public $settings;
@ -46,13 +48,13 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
* Create a new job instance.
*
*/
public function __construct($invoices, Company $company, $email)
public function __construct($invoices, Company $company, User $user)
{
$this->invoices = $invoices;
$this->company = $company;
$this->email = $email;
$this->user = $user;
$this->settings = $company->settings;
}
@ -90,14 +92,13 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
fclose($tempStream);
$this->setMailDriver();
$nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company);
$nmo->to_user = $this->user;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
try {
Mail::to($this->email)
->send(new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company));
} catch (\Exception $e) {
// //$this->failed($e);
}
NinjaMailerJob::dispatch($nmo);
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
}

View File

@ -13,17 +13,21 @@ namespace App\Jobs\Mail;
use App\DataMapper\Analytics\EmailFailure;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google;
use App\Libraries\MultiDB;
use App\Mail\TemplateEmail;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\User;
use App\Providers\MailServiceProvider;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -34,7 +38,6 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
/*Multi Mailer implemented*/
@ -78,10 +81,7 @@ class NinjaMailerJob implements ShouldQueue
nlog("error failed with {$e->getMessage()}");
if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($e->getMessage(), $this->nmo->to_user->client);
if($this->nmo->entity_string)
if($this->nmo->entity)
$this->entityEmailFailed($e->getMessage());
}
}
@ -89,15 +89,22 @@ class NinjaMailerJob implements ShouldQueue
/* Switch statement to handle failure notifications */
private function entityEmailFailed($message)
{
switch ($this->nmo->entity_string) {
case 'invoice':
$class = get_class($this->nmo->entity);
switch ($class) {
case Invoice::class:
event(new InvoiceWasEmailedAndFailed($this->nmo->invitation, $this->nmo->company, $message, $this->nmo->reminder_template, Ninja::eventVars()));
break;
case Payment::class:
event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars()));
break;
default:
# code...
break;
}
if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($message, $this->nmo->to_user->client);
}
private function setMailDriver()

View File

@ -35,4 +35,7 @@ class NinjaMailerObject
public $invitation = FALSE;
public $template = FALSE;
public $entity = FALSE;
}

View File

@ -13,7 +13,8 @@ namespace App\Jobs\Payment;
use App\Events\Payment\PaymentWasEmailed;
use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Jobs\Mail\BaseMailerJob;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Engine\PaymentEmailEngine;
use App\Mail\TemplateEmail;
@ -28,7 +29,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class EmailPayment extends BaseMailerJob implements ShouldQueue
class EmailPayment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -66,27 +67,24 @@ class EmailPayment extends BaseMailerJob implements ShouldQueue
*/
public function handle()
{
if ($this->company->is_disabled) {
if ($this->company->is_disabled)
return true;
}
if ($this->contact->email) {
MultiDB::setDb($this->company->db);
//if we need to set an email driver do it now
$this->setMailDriver();
MultiDB::setDb($this->company->db);
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
try {
$mail = Mail::to($this->contact->email, $this->contact->present()->name());
$mail->send(new TemplateEmail($email_builder, $this->contact));
} catch (\Exception $e) {
nlog("mailing failed with message " . $e->getMessage());
event(new PaymentWasEmailedAndFailed($this->payment, $this->company, Mail::failures(), Ninja::eventVars()));
//$this->failed($e);
return $this->logMailError($e->getMessage(), $this->payment->client);
}
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($email_builder, $this->contact);
$nmo->to_user = $this->contact;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
$nmo->entity = $this->payment;
NinjaMailerJob::dispatch($nmo);
event(new PaymentWasEmailed($this->payment, $this->payment->company, Ninja::eventVars()));
}

View File

@ -11,10 +11,12 @@
namespace App\Jobs\User;
use App\Jobs\Mail\BaseMailerJob;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\User\UserNotificationMailer;
use App\Models\Company;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -23,13 +25,13 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use stdClass;
class UserEmailChanged extends BaseMailerJob implements ShouldQueue
class UserEmailChanged implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $new_email;
protected $new_user;
protected $old_email;
protected $old_user;
protected $company;
@ -42,10 +44,10 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
* @param string $old_email
* @param Company $company
*/
public function __construct(string $new_email, string $old_email, Company $company)
public function __construct(User $new_user, User $old_user, Company $company)
{
$this->new_email = $new_email;
$this->old_email = $old_email;
$this->new_user = $new_user;
$this->old_user = $old_user;
$this->company = $company;
$this->settings = $this->company->settings;
}
@ -59,9 +61,6 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
//Set DB
MultiDB::setDb($this->company->db);
//If we need to set an email driver do it now
$this->setMailDriver();
/*Build the object*/
$mail_obj = new stdClass;
$mail_obj->subject = ctrans('texts.email_address_changed');
@ -71,17 +70,19 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
$mail_obj->data = $this->getData();
//Send email via a Mailable class
//
try {
Mail::to($this->old_email)
->send(new UserNotificationMailer($mail_obj));
Mail::to($this->new_email)
->send(new UserNotificationMailer($mail_obj));
} catch (\Exception $e) {
//$this->failed($e);
$this->logMailError($e->getMessage(), $this->company->owner());
}
$nmo = new NinjaMailerObject;
$nmo->mailable = new UserNotificationMailer($mail_obj);
$nmo->settings = $this->settings;
$nmo->company = $this->company;
$nmo->to_user = $this->old_user;
NinjaMailerJob::dispatch($nmo);
$nmo->to_user = $this->new_user;
NinjaMailerJob::dispatch($nmo);
}
private function getData()

View File

@ -24,9 +24,6 @@ class DownloadInvoices extends Mailable
/**
* Build the message.
*
* @return $this
* @throws \Laracasts\Presenter\Exceptions\PresenterException
*/
public function build()
{

View File

@ -1,10 +1,10 @@
<?php
/**
* client Ninja (https://clientninja.com).
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -8,250 +8,589 @@
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature\Import;
use App\Jobs\Import\CSVImport;
namespace App\Jobs\Import;
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Jobs\Mail\MailRouter;
use App\Libraries\MultiDB;
use App\Mail\Import\ImportCompleted;
use App\Models\Client;
use App\Models\Expense;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Country;
use App\Models\Currency;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\Product;
use App\Models\Project;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Vendor;
use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests;
use App\Repositories\BaseRepository;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Utils\Traits\CleanLineItems;
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\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\Csv\Reader;
use League\Csv\Statement;
use Tests\MockAccountData;
use Tests\TestCase;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
/**
* @test
* @covers App\Http\Controllers\ImportController
class CSVImport implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems;
public $invoice;
public $company;
public $hash;
public $import_type;
public $skip_header;
public $column_map;
public $import_array;
public $error_array = [];
public $maps;
public function __construct( array $request, Company $company ) {
$this->company = $company;
$this->hash = $request['hash'];
$this->import_type = $request['import_type'];
$this->skip_header = $request['skip_header'] ?? null;
$this->column_map = $request['column_map'] ?? null;
}
/**
* Execute the job.
*
*
* @return void
*/
class ImportCsvTest extends TestCase
{
use MakesHash;
use MockAccountData;
public function handle() {
public function setUp() :void
{
parent::setUp();
MultiDB::setDb( $this->company->db );
$this->withoutMiddleware(
ThrottleRequests::class
$this->company->owner()->setCompany( $this->company );
Auth::login( $this->company->owner(), true );
$this->buildMaps();
nlog( "import " . $this->import_type );
foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entityType ) {
$csvData = $this->getCsvData( $entityType );
if ( ! empty( $csvData ) ) {
$importFunction = "import" . Str::plural( Str::title( $entityType ) );
$preTransformFunction = "preTransform" . Str::title( $this->import_type );
if ( method_exists( $this, $preTransformFunction ) ) {
$csvData = $this->$preTransformFunction( $csvData, $entityType );
}
if ( empty( $csvData ) ) {
continue;
}
if ( method_exists( $this, $importFunction ) ) {
// If there's an entity-specific import function, use that.
$this->$importFunction( $csvData );
} else {
// Otherwise, use the generic import function.
$this->importEntities( $csvData, $entityType );
}
}
}
$data = [
'errors' => $this->error_array,
'company' => $this->company,
];
MailRouter::dispatch( new ImportCompleted( $data ), $this->company, auth()->user() );
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function preTransformCsv( $csvData, $entityType ) {
if ( empty( $this->column_map[ $entityType ] ) ) {
return false;
}
if ( $this->skip_header ) {
array_shift( $csvData );
}
//sort the array by key
$keys = $this->column_map[ $entityType ];
ksort( $keys );
$csvData = array_map( function ( $row ) use ( $keys ) {
return array_combine( $keys, array_intersect_key( $row, $keys ) );
}, $csvData );
if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'invoice.number' );
}
return $csvData;
}
private function preTransformFreshbooks( $csvData, $entityType ) {
$csvData = $this->mapCSVHeaderToKeys( $csvData );
if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'Invoice #' );
}
return $csvData;
}
private function preTransformInvoicely( $csvData, $entityType ) {
$csvData = $this->mapCSVHeaderToKeys( $csvData );
return $csvData;
}
private function preTransformInvoice2go( $csvData, $entityType ) {
$csvData = $this->mapCSVHeaderToKeys( $csvData );
return $csvData;
}
private function preTransformZoho( $csvData, $entityType ) {
$csvData = $this->mapCSVHeaderToKeys( $csvData );
if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'Invoice Number' );
}
return $csvData;
}
private function preTransformWaveaccounting( $csvData, $entityType ) {
$csvData = $this->mapCSVHeaderToKeys( $csvData );
if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'Invoice Number' );
}
return $csvData;
}
private function groupInvoices( $csvData, $key ) {
// Group by invoice.
$grouped = [];
foreach ( $csvData as $line_item ) {
if ( empty( $line_item[ $key ] ) ) {
$this->error_array['invoice'][] = [ 'invoice' => $line_item, 'error' => 'No invoice number' ];
} else {
$grouped[ $line_item[ $key ] ][] = $line_item;
}
}
return $grouped;
}
private function mapCSVHeaderToKeys( $csvData ) {
$keys = array_shift( $csvData );
return array_map( function ( $values ) use ( $keys ) {
return array_combine( $keys, $values );
}, $csvData );
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function importInvoices( $invoices ) {
$invoice_transformer = $this->getTransformer( 'invoice' );
/** @var PaymentRepository $payment_repository */
$payment_repository = app()->make( PaymentRepository::class );
$payment_repository->import_mode = true;
/** @var ClientRepository $client_repository */
$client_repository = app()->make( ClientRepository::class );
$client_repository->import_mode = true;
$invoice_repository = new InvoiceRepository();
$invoice_repository->import_mode = true;
foreach ( $invoices as $raw_invoice ) {
try {
$invoice_data = $invoice_transformer->transform( $raw_invoice );
$invoice_data['line_items'] = $this->cleanItems( $invoice_data['line_items'] ?? [] );
// If we don't have a client ID, but we do have client data, go ahead and create the client.
if ( empty( $invoice_data['client_id'] ) && ! empty( $invoice_data['client'] ) ) {
$client_data = $invoice_data['client'];
$client_data['user_id'] = $this->getUserIDForRecord( $invoice_data );
$client_repository->save(
$client_data,
$client = ClientFactory::create( $this->company->id, $client_data['user_id'] )
);
// $this->faker = \Faker\Factory::create();
$this->makeTestData();
$this->withoutExceptionHandling();
$invoice_data['client_id'] = $client->id;
unset( $invoice_data['client'] );
}
public function testCsvRead()
{
$csv = file_get_contents(base_path().'/tests/Feature/Import/invoice.csv');
$validator = Validator::make( $invoice_data, ( new StoreInvoiceRequest() )->rules() );
if ( $validator->fails() ) {
$this->error_array['invoice'][] =
[ 'invoice' => $invoice_data, 'error' => $validator->errors()->all() ];
} else {
$invoice = InvoiceFactory::create( $this->company->id, $this->getUserIDForRecord( $invoice_data ) );
if ( ! empty( $invoice_data['status_id'] ) ) {
$invoice->status_id = $invoice_data['status_id'];
}
$invoice_repository->save( $invoice_data, $invoice );
$this->addInvoiceToMaps( $invoice );
$this->assertTrue(is_array($this->getCsvData($csv)));
// If we're doing a generic CSV import, only import payment data if we're not importing a payment CSV.
// If we're doing a platform-specific import, trust the platform to only return payment info if there's not a separate payment CSV.
if ( $this->import_type !== 'csv' || empty( $this->column_map['payment'] ) ) {
// Check for payment columns
if ( ! empty( $invoice_data['payments'] ) ) {
foreach ( $invoice_data['payments'] as $payment_data ) {
$payment_data['user_id'] = $invoice->user_id;
$payment_data['client_id'] = $invoice->client_id;
$payment_data['invoices'] = [
[
'invoice_id' => $invoice->id,
'amount' => $payment_data['amount'] ?? null,
],
];
$payment_repository->save(
$payment_data,
PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id )
);
}
}
}
public function testClientCsvImport()
{
$csv = file_get_contents(base_path().'/tests/Feature/Import/clients.csv');
$hash = Str::random(32);
$column_map = [
1 => 'client.balance',
2 => 'client.paid_to_date',
0 => 'client.name',
19 => 'client.currency_id',
20 => 'client.public_notes',
21 => 'client.private_notes',
22 => 'contact.first_name',
23 => 'contact.last_name',
];
$data = [
'hash' => $hash,
'column_map' => [ 'client' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Client::count();
Cache::put( $hash . '-client', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Client::count() );
$this->actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository );
}
} catch ( \Exception $ex ) {
if ( $ex instanceof ImportException ) {
$message = $ex->getMessage();
} else {
report( $ex );
$message = 'Unknown error';
}
public function testInvoiceCsvImport()
{
$csv = file_get_contents(base_path().'/tests/Feature/Import/invoice.csv');
$hash = Str::random(32);
$column_map = [
1 => 'client.email',
3 => 'payment.amount',
5 => 'invoice.po_number',
8 => 'invoice.due_date',
9 => 'item.discount',
11 => 'invoice.partial_due_date',
12 => 'invoice.public_notes',
13 => 'invoice.private_notes',
0 => 'client.name',
2 => 'invoice.number',
7 => 'invoice.date',
14 => 'item.product_key',
15 => 'item.notes',
16 => 'item.cost',
17 => 'item.quantity',
];
$data = [
'hash' => $hash,
'column_map' => [ 'invoice' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Invoice::count();
Cache::put( $hash . '-invoice', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Invoice::count() );
$this->error_array['invoice'][] = [ 'invoice' => $raw_invoice, 'error' => $message ];
}
}
}
public function testVendorCsvImport() {
$csv = file_get_contents( base_path() . '/tests/Feature/Import/vendors.csv' );
$hash = Str::random( 32 );
$column_map = [
0 => 'vendor.name',
19 => 'vendor.currency_id',
20 => 'vendor.public_notes',
21 => 'vendor.private_notes',
22 => 'vendor.first_name',
23 => 'vendor.last_name',
];
$data = [
'hash' => $hash,
'column_map' => [ 'vendor' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Vendor::count();
Cache::put( $hash . '-vendor', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Vendor::count() );
private function actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ) {
if ( ! empty( $invoice_data['archived'] ) ) {
$invoice_repository->archive( $invoice );
$invoice->fresh();
}
public function testProductCsvImport() {
$csv = file_get_contents( base_path() . '/tests/Feature/Import/products.csv' );
$hash = Str::random( 32 );
$column_map = [
2 => 'product.notes',
3 => 'product.cost',
];
$data = [
'hash' => $hash,
'column_map' => [ 'product' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Product::count();
Cache::put( $hash . '-product', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Product::count() );
if ( ! empty( $invoice_data['viewed'] ) ) {
$invoice = $invoice->service()->markViewed()->save();
}
public function testExpenseCsvImport() {
$csv = file_get_contents( base_path() . '/tests/Feature/Import/expenses.csv' );
$hash = Str::random( 32 );
$column_map = [
2 => 'expense.public_notes',
3 => 'expense.amount',
];
$data = [
'hash' => $hash,
'column_map' => [ 'expense' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Expense::count();
Cache::put( $hash . '-expense', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Expense::count() );
if ( $invoice->status_id === Invoice::STATUS_SENT ) {
$invoice = $invoice->service()->markSent()->save();
}
public function testPaymentCsvImport() {
$csv = file_get_contents( base_path() . '/tests/Feature/Import/payments.csv' );
$hash = Str::random( 32 );
$column_map = [
0 => 'payment.client_id',
1 => 'payment.invoice_number',
2 => 'payment.amount',
3 => 'payment.date',
];
$data = [
'hash' => $hash,
'column_map' => [ 'payment' => $column_map ],
'skip_header' => true,
'import_type' => 'csv',
];
$pre_import = Payment::count();
Cache::put( $hash . '-payment', base64_encode( $csv ), 360 );
CSVImport::dispatchNow( $data, $this->company );
$this->assertGreaterThan( $pre_import, Payment::count() );
if ( $invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0 ) {
if ( $invoice->balance < $invoice->amount ) {
$invoice->status_id = Invoice::STATUS_PARTIAL;
$invoice->save();
} elseif ( $invoice->balance <= 0 ) {
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
}
}
private function getCsvData($csvfile)
{
if (! ini_get('auto_detect_line_endings')) {
ini_set('auto_detect_line_endings', '1');
return $invoice;
}
$csv = Reader::createFromString($csvfile);
private function importEntities( $records, $entity_type ) {
$entity_type = Str::slug( $entity_type, '_' );
$formatted_entity_type = Str::title( $entity_type );
$request_name = "\\App\\Http\\Requests\\${formatted_entity_type}\\Store${formatted_entity_type}Request";
$repository_name = '\\App\\Repositories\\' . $formatted_entity_type . 'Repository';
$factoryName = '\\App\\Factory\\' . $formatted_entity_type . 'Factory';
/** @var BaseRepository $repository */
$repository = app()->make( $repository_name );
$repository->import_mode = true;
$transformer = $this->getTransformer( $entity_type );
foreach ( $records as $record ) {
try {
$entity = $transformer->transform( $record );
/** @var \App\Http\Requests\Request $request */
$request = new $request_name();
// Pass entity data to request so it can be validated
$request->query = $request->request = new ParameterBag( $entity );
$validator = Validator::make( $entity, $request->rules() );
if ( $validator->fails() ) {
$this->error_array[ $entity_type ][] =
[ $entity_type => $record, 'error' => $validator->errors()->all() ];
} else {
$entity =
$repository->save(
array_diff_key( $entity, [ 'user_id' => false ] ),
$factoryName::create( $this->company->id, $this->getUserIDForRecord( $entity ) ) );
$entity->save();
if ( method_exists( $this, 'add' . $formatted_entity_type . 'ToMaps' ) ) {
$this->{'add' . $formatted_entity_type . 'ToMaps'}( $entity );
}
}
} catch ( \Exception $ex ) {
if ( $ex instanceof ImportException ) {
$message = $ex->getMessage();
} else {
report( $ex );
$message = 'Unknown error';
}
$this->error_array[ $entity_type ][] = [ $entity_type => $record, 'error' => $message ];
}
}
}
/**
* @param $entity_type
*
* @return BaseTransformer
*/
private function getTransformer( $entity_type ) {
$formatted_entity_type = Str::title( $entity_type );
$formatted_import_type = Str::title( $this->import_type );
$transformer_name =
'\\App\\Import\\Transformers\\' . $formatted_import_type . '\\' . $formatted_entity_type . 'Transformer';
return new $transformer_name( $this->maps );
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function buildMaps() {
$this->maps = [
'company' => $this->company,
'client' => [],
'contact' => [],
'invoice' => [],
'invoice_client' => [],
'product' => [],
'countries' => [],
'countries2' => [],
'currencies' => [],
'client_ids' => [],
'invoice_ids' => [],
'vendors' => [],
'expense_categories' => [],
'payment_types' => [],
'tax_rates' => [],
'tax_names' => [],
];
$clients = Client::scope()->get();
foreach ( $clients as $client ) {
$this->addClientToMaps( $client );
}
$contacts = ClientContact::scope()->get();
foreach ( $contacts as $contact ) {
$this->addContactToMaps( $contact );
}
$invoices = Invoice::scope()->get();
foreach ( $invoices as $invoice ) {
$this->addInvoiceToMaps( $invoice );
}
$products = Product::scope()->get();
foreach ( $products as $product ) {
$this->addProductToMaps( $product );
}
$projects = Project::scope()->get();
foreach ( $projects as $project ) {
$this->addProjectToMaps( $project );
}
$countries = Country::all();
foreach ( $countries as $country ) {
$this->maps['countries'][ strtolower( $country->name ) ] = $country->id;
$this->maps['countries2'][ strtolower( $country->iso_3166_2 ) ] = $country->id;
}
$currencies = Currency::all();
foreach ( $currencies as $currency ) {
$this->maps['currencies'][ strtolower( $currency->code ) ] = $currency->id;
}
$payment_types = PaymentType::all();
foreach ( $payment_types as $payment_type ) {
$this->maps['payment_types'][ strtolower( $payment_type->name ) ] = $payment_type->id;
}
$vendors = Vendor::scope()->get();
foreach ( $vendors as $vendor ) {
$this->addVendorToMaps( $vendor );
}
$expenseCaegories = ExpenseCategory::scope()->get();
foreach ( $expenseCaegories as $category ) {
$this->addExpenseCategoryToMaps( $category );
}
$taxRates = TaxRate::scope()->get();
foreach ( $taxRates as $taxRate ) {
$name = trim( strtolower( $taxRate->name ) );
$this->maps['tax_rates'][ $name ] = $taxRate->rate;
$this->maps['tax_names'][ $name ] = $taxRate->name;
}
}
/**
* @param Invoice $invoice
*/
private function addInvoiceToMaps( Invoice $invoice ) {
if ( $number = strtolower( trim( $invoice->number ) ) ) {
$this->maps['invoices'][ $number ] = $invoice;
$this->maps['invoice'][ $number ] = $invoice->id;
$this->maps['invoice_client'][ $number ] = $invoice->client_id;
$this->maps['invoice_ids'][ $invoice->public_id ] = $invoice->id;
}
}
/**
* @param Client $client
*/
private function addClientToMaps( Client $client ) {
if ( $name = strtolower( trim( $client->name ) ) ) {
$this->maps['client'][ $name ] = $client->id;
$this->maps['client_ids'][ $client->public_id ] = $client->id;
}
if ( $client->contacts->count() ) {
$contact = $client->contacts[0];
if ( $email = strtolower( trim( $contact->email ) ) ) {
$this->maps['client'][ $email ] = $client->id;
}
if ( $name = strtolower( trim( $contact->first_name . ' ' . $contact->last_name ) ) ) {
$this->maps['client'][ $name ] = $client->id;
}
$this->maps['client_ids'][ $client->public_id ] = $client->id;
}
}
/**
* @param ClientContact $contact
*/
private function addContactToMaps( ClientContact $contact ) {
if ( $key = strtolower( trim( $contact->email ) ) ) {
$this->maps['contact'][ $key ] = $contact;
}
}
/**
* @param Product $product
*/
private function addProductToMaps( Product $product ) {
if ( $key = strtolower( trim( $product->product_key ) ) ) {
$this->maps['product'][ $key ] = $product;
}
}
/**
* @param Project $project
*/
private function addProjectToMaps( Project $project ) {
if ( $key = strtolower( trim( $project->name ) ) ) {
$this->maps['project'][ $key ] = $project;
}
}
private function addVendorToMaps( Vendor $vendor ) {
$this->maps['vendor'][ strtolower( $vendor->name ) ] = $vendor->id;
}
private function addExpenseCategoryToMaps( ExpenseCategory $category ) {
if ( $name = strtolower( $category->name ) ) {
$this->maps['expense_category'][ $name ] = $category->id;
}
}
private function getUserIDForRecord( $record ) {
if ( ! empty( $record['user_id'] ) ) {
return $this->findUser( $record['user_id'] );
} else {
return $this->company->owner()->id;
}
}
private function findUser( $user_hash ) {
$user = User::where( 'company_id', $this->company->id )
->where( \DB::raw( 'CONCAT_WS(" ", first_name, last_name)' ), 'like', '%' . $user_hash . '%' )
->first();
if ( $user ) {
return $user->id;
} else {
return $this->company->owner()->id;
}
}
private function getCsvData( $entityType ) {
$base64_encoded_csv = Cache::get( $this->hash . '-' . $entityType );
if ( empty( $base64_encoded_csv ) ) {
return null;
}
$csv = base64_decode( $base64_encoded_csv );
$csv = Reader::createFromString( $csv );
$stmt = new Statement();
$data = iterator_to_array($stmt->process($csv));
$data = iterator_to_array( $stmt->process( $csv ) );
if (count($data) > 0) {
if ( count( $data ) > 0 ) {
$headers = $data[0];
// Remove Invoice Ninja headers
if (count($headers) && count($data) > 4) {
if ( count( $headers ) && count( $data ) > 4 && $this->import_type === 'csv' ) {
$firstCell = $headers[0];
if (strstr($firstCell, config('ninja.app_name'))) {
array_shift($data); // Invoice Ninja...
array_shift($data); // <blank line>
array_shift($data); // Enitty Type Header
if ( strstr( $firstCell, config( 'ninja.app_name' ) ) ) {
array_shift( $data ); // Invoice Ninja...
array_shift( $data ); // <blank line>
array_shift( $data ); // Enitty Type Header
}
}
}