diff --git a/app/Events/Payment/PaymentWasEmailedAndFailed.php b/app/Events/Payment/PaymentWasEmailedAndFailed.php index 8cf8782db02f..6270bb1328fe 100644 --- a/app/Events/Payment/PaymentWasEmailedAndFailed.php +++ b/app/Events/Payment/PaymentWasEmailedAndFailed.php @@ -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; diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 460c018811e3..a5e99de0b00e 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -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); } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 7f6cc661bc3b..5b777216336a 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -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); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index bc84f38f12e3..0f8cfd3c58d4 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -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,10 +377,9 @@ 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 || $old_company_user->is_admin != $user->company_user->is_admin diff --git a/app/Import/Definitions/ClientMap.php b/app/Import/Definitions/ClientMap.php index 912f1c415347..27c0873352c6 100644 --- a/app/Import/Definitions/ClientMap.php +++ b/app/Import/Definitions/ClientMap.php @@ -1,10 +1,10 @@ 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, + ]; + } +} diff --git a/app/Import/Transformers/Csv/ProductTransformer.php b/app/Import/Transformers/Csv/ProductTransformer.php index 451c33554a09..6b436ccba676 100644 --- a/app/Import/Transformers/Csv/ProductTransformer.php +++ b/app/Import/Transformers/Csv/ProductTransformer.php @@ -1,10 +1,10 @@ $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'), + ]; + } +} diff --git a/app/Import/Transformers/InvoiceTransformer.php b/app/Import/Transformers/InvoiceTransformer.php new file mode 100644 index 000000000000..bea2a3caa3ed --- /dev/null +++ b/app/Import/Transformers/InvoiceTransformer.php @@ -0,0 +1,61 @@ + $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'), + ]; + } +} diff --git a/app/Import/Transformers/PaymentTransformer.php b/app/Import/Transformers/PaymentTransformer.php new file mode 100644 index 000000000000..c2476ce685a3 --- /dev/null +++ b/app/Import/Transformers/PaymentTransformer.php @@ -0,0 +1,46 @@ + $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 + ]; + } +} diff --git a/app/Jobs/Entity/EmailEntity.php b/app/Jobs/Entity/EmailEntity.php index b4d8ca973748..95c16acbf030 100644 --- a/app/Jobs/Entity/EmailEntity.php +++ b/app/Jobs/Entity/EmailEntity.php @@ -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,7 +112,8 @@ 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); /* Mark entity sent */ diff --git a/app/Jobs/Import/CSVImport.php b/app/Jobs/Import/CSVImport.php index e0fc289ad8d6..12acb0169349 100644 --- a/app/Jobs/Import/CSVImport.php +++ b/app/Jobs/Import/CSVImport.php @@ -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 ] ) ) { diff --git a/app/Jobs/Invoice/ZipInvoices.php b/app/Jobs/Invoice/ZipInvoices.php index 01fb041a7cb3..4d1035db9375 100644 --- a/app/Jobs/Invoice/ZipInvoices.php +++ b/app/Jobs/Invoice/ZipInvoices.php @@ -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(); - - 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); - } + $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; + + NinjaMailerJob::dispatch($nmo); UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1)); } diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 2f858564a99e..598c0cfcd0c9 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -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() diff --git a/app/Jobs/Mail/NinjaMailerObject.php b/app/Jobs/Mail/NinjaMailerObject.php index 0d8007608b83..ec34c5f39236 100644 --- a/app/Jobs/Mail/NinjaMailerObject.php +++ b/app/Jobs/Mail/NinjaMailerObject.php @@ -35,4 +35,7 @@ class NinjaMailerObject public $invitation = FALSE; public $template = FALSE; + + public $entity = FALSE; + } diff --git a/app/Jobs/Payment/EmailPayment.php b/app/Jobs/Payment/EmailPayment.php index 2530636ee993..134b0673eac6 100644 --- a/app/Jobs/Payment/EmailPayment.php +++ b/app/Jobs/Payment/EmailPayment.php @@ -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())); } diff --git a/app/Jobs/User/UserEmailChanged.php b/app/Jobs/User/UserEmailChanged.php index 425df5481e9e..b4021fe1cb6a 100644 --- a/app/Jobs/User/UserEmailChanged.php +++ b/app/Jobs/User/UserEmailChanged.php @@ -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)); + + $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); - Mail::to($this->new_email) - ->send(new UserNotificationMailer($mail_obj)); - } catch (\Exception $e) { - //$this->failed($e); - $this->logMailError($e->getMessage(), $this->company->owner()); - } } private function getData() diff --git a/app/Mail/DownloadInvoices.php b/app/Mail/DownloadInvoices.php index 70237def6ec0..006703f1f382 100644 --- a/app/Mail/DownloadInvoices.php +++ b/app/Mail/DownloadInvoices.php @@ -24,9 +24,6 @@ class DownloadInvoices extends Mailable /** * Build the message. - * - * @return $this - * @throws \Laracasts\Presenter\Exceptions\PresenterException */ public function build() { diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 9023f3aa53c7..f144c591f4f4 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -1,10 +1,10 @@ withoutMiddleware( - ThrottleRequests::class - ); + public $company; - // $this->faker = \Faker\Factory::create(); + public $hash; - $this->makeTestData(); + public $import_type; - $this->withoutExceptionHandling(); - } + public $skip_header; - public function testCsvRead() - { - $csv = file_get_contents(base_path().'/tests/Feature/Import/invoice.csv'); + public $column_map; - $this->assertTrue(is_array($this->getCsvData($csv))); - } + public $import_array; - 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', - ]; + public $error_array = []; - $data = [ - 'hash' => $hash, - 'column_map' => [ 'client' => $column_map ], - 'skip_header' => true, - 'import_type' => 'csv', - ]; + public $maps; - $pre_import = Client::count(); - - Cache::put( $hash . '-client', base64_encode( $csv ), 360 ); - - CSVImport::dispatchNow( $data, $this->company ); - - $this->assertGreaterThan( $pre_import, Client::count() ); + 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; } - public function testInvoiceCsvImport() - { - $csv = file_get_contents(base_path().'/tests/Feature/Import/invoice.csv'); - $hash = Str::random(32); + /** + * Execute the job. + * + * + * @return void + */ + public function handle() { - $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', - ]; + MultiDB::setDb( $this->company->db ); + + $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 = [ - 'hash' => $hash, - 'column_map' => [ 'invoice' => $column_map ], - 'skip_header' => true, - 'import_type' => 'csv', + 'errors' => $this->error_array, + 'company' => $this->company, ]; - $pre_import = Invoice::count(); - - Cache::put( $hash . '-invoice', base64_encode( $csv ), 360 ); - - CSVImport::dispatchNow( $data, $this->company ); - - $this->assertGreaterThan( $pre_import, Invoice::count() ); + MailRouter::dispatch( new ImportCompleted( $data ), $this->company, auth()->user() ); } - 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', - ]; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private function preTransformCsv( $csvData, $entityType ) { + if ( empty( $this->column_map[ $entityType ] ) ) { + return false; + } - $data = [ - 'hash' => $hash, - 'column_map' => [ 'vendor' => $column_map ], - 'skip_header' => true, - 'import_type' => 'csv', - ]; + if ( $this->skip_header ) { + array_shift( $csvData ); + } - $pre_import = Vendor::count(); + //sort the array by key + $keys = $this->column_map[ $entityType ]; + ksort( $keys ); - Cache::put( $hash . '-vendor', base64_encode( $csv ), 360 ); + $csvData = array_map( function ( $row ) use ( $keys ) { + return array_combine( $keys, array_intersect_key( $row, $keys ) ); + }, $csvData ); - CSVImport::dispatchNow( $data, $this->company ); + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'invoice.number' ); + } - $this->assertGreaterThan( $pre_import, Vendor::count() ); + return $csvData; } - public function testProductCsvImport() { - $csv = file_get_contents( base_path() . '/tests/Feature/Import/products.csv' ); - $hash = Str::random( 32 ); + private function preTransformFreshbooks( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - $column_map = [ - 2 => 'product.notes', - 3 => 'product.cost', - ]; + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice #' ); + } - $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() ); + return $csvData; } - public function testExpenseCsvImport() { - $csv = file_get_contents( base_path() . '/tests/Feature/Import/expenses.csv' ); - $hash = Str::random( 32 ); + private function preTransformInvoicely( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - $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() ); + return $csvData; } - public function testPaymentCsvImport() { - $csv = file_get_contents( base_path() . '/tests/Feature/Import/payments.csv' ); - $hash = Str::random( 32 ); + private function preTransformInvoice2go( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - $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() ); + return $csvData; } - private function getCsvData($csvfile) - { - if (! ini_get('auto_detect_line_endings')) { - ini_set('auto_detect_line_endings', '1'); - } + private function preTransformZoho( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - $csv = Reader::createFromString($csvfile); - $stmt = new Statement(); - $data = iterator_to_array($stmt->process($csv)); + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice Number' ); + } - if (count($data) > 0) { - $headers = $data[0]; + return $csvData; + } - // Remove Invoice Ninja headers - if (count($headers) && count($data) > 4) { - $firstCell = $headers[0]; - if (strstr($firstCell, config('ninja.app_name'))) { - array_shift($data); // Invoice Ninja... - array_shift($data); // - array_shift($data); // Enitty Type Header - } - } - } + private function preTransformWaveaccounting( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - return $data; - } -} + 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'] ) + ); + $invoice_data['client_id'] = $client->id; + unset( $invoice_data['client'] ); + } + + $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 ); + + // 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 ) + ); + } + } + } + + $this->actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ); + } + } catch ( \Exception $ex ) { + if ( $ex instanceof ImportException ) { + $message = $ex->getMessage(); + } else { + report( $ex ); + $message = 'Unknown error'; + } + + $this->error_array['invoice'][] = [ 'invoice' => $raw_invoice, 'error' => $message ]; + } + } + } + + private function actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ) { + if ( ! empty( $invoice_data['archived'] ) ) { + $invoice_repository->archive( $invoice ); + $invoice->fresh(); + } + + if ( ! empty( $invoice_data['viewed'] ) ) { + $invoice = $invoice->service()->markViewed()->save(); + } + + if ( $invoice->status_id === Invoice::STATUS_SENT ) { + $invoice = $invoice->service()->markSent()->save(); + } + + 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(); + } + } + + + return $invoice; + } + + 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 ) ); + + if ( count( $data ) > 0 ) { + $headers = $data[0]; + + // Remove Invoice Ninja headers + 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 ); // + array_shift( $data ); // Enitty Type Header + } + } + } + + return $data; + } +} \ No newline at end of file