diff --git a/CHANGELOG.md b/CHANGELOG.md index 91283f15601a..05750485054f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release notes ## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop) + +## Added: +- Invoice / Quote / Credit created notifications +- Logout route - deletes all auth tokens + +## [v5.1.54-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.50-release) ## Fixed: - Fixes for e-mails, encoding & parsing invalid HTML diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 12cdb5ef6283..81dd5fffadd3 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -16,6 +16,7 @@ use App\DataMapper\Analytics\LoginSuccess; use App\Http\Controllers\BaseController; use App\Http\Controllers\Controller; use App\Jobs\Account\CreateAccount; +use App\Jobs\Company\CreateCompanyToken; use App\Libraries\MultiDB; use App\Libraries\OAuth\OAuth; use App\Libraries\OAuth\Providers\Google; @@ -199,6 +200,15 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); + $cu->first()->account->companies->each(function ($company) use($cu, $request){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')); + } + + }); + return $this->listResponse($cu); } else { @@ -262,9 +272,16 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', $company_token->user_id); - //->where('company_id', $company_token->company_id); - //$ct = CompanyUser::whereUserId(auth()->user()->id); + + $cu->first()->account->companies->each(function ($company) use($cu, $request){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')); + } + }); + return $this->refreshResponse($cu); } @@ -317,6 +334,14 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); + $cu->first()->account->companies->each(function ($company) use($cu){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); + } + }); + return $this->listResponse($cu); } @@ -348,9 +373,17 @@ class LoginController extends BaseController $timeout = auth()->user()->company()->default_password_timeout / 60000; Cache::put(auth()->user()->hashed_id.'_logged_in', Str::random(64), $timeout); - $ct = CompanyUser::whereUserId(auth()->user()->id); + $cu = CompanyUser::whereUserId(auth()->user()->id); - return $this->listResponse($ct); + $cu->first()->account->companies->each(function ($company) use($cu){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); + } + }); + + return $this->listResponse($cu); } return response() diff --git a/app/Listeners/Credit/CreditCreatedNotification.php b/app/Listeners/Credit/CreditCreatedNotification.php new file mode 100644 index 000000000000..123b32550719 --- /dev/null +++ b/app/Listeners/Credit/CreditCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $credit = $event->credit; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($credit, 'credit'))->build() ); + $nmo->company = $credit->company; + $nmo->settings = $credit->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'credit'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($credit->invitations()->first(), $company_user, 'credit', ['all_notifications', 'credit_created', 'credit_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + unset($methods[$key]); + + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + + /* Override the methods in the Notification Class */ + // $notification->method = $methods; + + // Notify on the alternate channels + // $user->notify($notification); + } + } +} diff --git a/app/Listeners/Invoice/InvoiceCreatedNotification.php b/app/Listeners/Invoice/InvoiceCreatedNotification.php new file mode 100644 index 000000000000..7008f83c912f --- /dev/null +++ b/app/Listeners/Invoice/InvoiceCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $invoice = $event->invoice; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($invoice, 'invoice'))->build() ); + $nmo->company = $invoice->company; + $nmo->settings = $invoice->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'invoice'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($invoice->invitations()->first(), $company_user, 'invoice', ['all_notifications', 'invoice_created', 'invoice_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + unset($methods[$key]); + + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + + /* Override the methods in the Notification Class */ + // $notification->method = $methods; + + // Notify on the alternate channels + // $user->notify($notification); + } + } +} diff --git a/app/Listeners/Quote/QuoteCreatedNotification.php b/app/Listeners/Quote/QuoteCreatedNotification.php new file mode 100644 index 000000000000..867a1daa9c4f --- /dev/null +++ b/app/Listeners/Quote/QuoteCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $quote = $event->quote; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($quote, 'quote'))->build() ); + $nmo->company = $quote->company; + $nmo->settings = $quote->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'quote'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_created', 'quote_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + unset($methods[$key]); + + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + + /* Override the methods in the Notification Class */ + // $notification->method = $methods; + + // Notify on the alternate channels + // $user->notify($notification); + } + } +} diff --git a/app/Mail/Admin/EntityCreatedObject.php b/app/Mail/Admin/EntityCreatedObject.php new file mode 100644 index 000000000000..f33c7cd540ce --- /dev/null +++ b/app/Mail/Admin/EntityCreatedObject.php @@ -0,0 +1,127 @@ +entity_type = $entity_type; + $this->entity = $entity; + } + + public function build() + { + + $this->contact = $this->entity->invitations()->first()->contact; + $this->company = $this->entity->company; + + $this->setTemplate(); + + $mail_obj = new stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } + + private function setTemplate() + { + // nlog($this->template); + + switch ($this->entity_type) { + case 'invoice': + $this->template_subject = "texts.notification_invoice_created_subject"; + $this->template_body = "texts.notification_invoice_sent"; + break; + case 'quote': + $this->template_subject = "texts.notification_quote_created_subject"; + $this->template_body = "texts.notification_quote_sent"; + break; + case 'credit': + $this->template_subject = "texts.notification_credit_created_subject"; + $this->template_body = "texts.notification_credit_sent"; + break; + + default: + $this->template_subject = "texts.notification_invoice_created_subject"; + $this->template_body = "texts.notification_invoice_sent"; + break; + } + } + + private function getAmount() + { + return Number::formatMoney($this->entity->amount, $this->entity->client); + } + + private function getSubject() + { + return + ctrans( + $this->template_subject, + [ + 'client' => $this->contact->present()->name(), + 'invoice' => $this->entity->number, + ] + ); + } + + private function getMessage() + { + return ctrans( + $this->template_body, + [ + 'amount' => $this->getAmount(), + 'client' => $this->contact->present()->name(), + 'invoice' => $this->entity->number, + ] + ); + } + + private function getData() + { + $settings = $this->entity->client->getMergedSettings(); + + return [ + 'title' => $this->getSubject(), + 'message' => $this->getMessage(), + 'url' => $this->entity->invitations()->first()->getAdminLink(), + 'button' => ctrans("texts.view_{$this->entity_type}"), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index ca4706fc599e..a5f0e5c40dc2 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -127,6 +127,7 @@ use App\Listeners\Activity\VendorDeletedActivity; use App\Listeners\Activity\VendorRestoredActivity; use App\Listeners\Activity\VendorUpdatedActivity; use App\Listeners\Contact\UpdateContactLastLogin; +use App\Listeners\Credit\CreditCreatedNotification; use App\Listeners\Credit\CreditEmailedNotification; use App\Listeners\Credit\CreditRestoredActivity; use App\Listeners\Credit\CreditViewedActivity; @@ -136,6 +137,7 @@ use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoicePdf; use App\Listeners\Invoice\InvoiceArchivedActivity; use App\Listeners\Invoice\InvoiceCancelledActivity; +use App\Listeners\Invoice\InvoiceCreatedNotification; use App\Listeners\Invoice\InvoiceDeletedActivity; use App\Listeners\Invoice\InvoiceEmailActivity; use App\Listeners\Invoice\InvoiceEmailFailedActivity; @@ -155,6 +157,7 @@ use App\Listeners\Payment\PaymentNotification; use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Quote\QuoteApprovedActivity; use App\Listeners\Quote\QuoteArchivedActivity; +use App\Listeners\Quote\QuoteCreatedNotification; use App\Listeners\Quote\QuoteDeletedActivity; use App\Listeners\Quote\QuoteEmailActivity; use App\Listeners\Quote\QuoteEmailedNotification; @@ -260,6 +263,7 @@ class EventServiceProvider extends ServiceProvider ], CreditWasCreated::class => [ CreatedCreditActivity::class, + CreditCreatedNotification::class, ], CreditWasDeleted::class => [ DeleteCreditActivity::class, @@ -318,6 +322,7 @@ class EventServiceProvider extends ServiceProvider ], InvoiceWasCreated::class => [ CreateInvoiceActivity::class, + InvoiceCreatedNotification::class, // CreateInvoicePdf::class, ], InvoiceWasPaid::class => [ @@ -373,6 +378,7 @@ class EventServiceProvider extends ServiceProvider ], QuoteWasCreated::class => [ CreatedQuoteActivity::class, + QuoteCreatedNotification::class, ], QuoteWasUpdated::class => [ QuoteUpdatedActivity::class, diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 6260c7b01f1c..2619e20330fc 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4204,6 +4204,22 @@ $LANG = array( 'promo_code' => 'Promo code', 'recurring_invoice_issued_to' => 'Recurring invoice issued to', 'subscription' => 'Subscription', + 'new_subscription' => 'New Subscription', + 'deleted_subscription' => 'Successfully deleted subscription', + 'removed_subscription' => 'Successfully removed subscription', + 'restored_subscription' => 'Successfully restored subscription', + 'search_subscription' => 'Search 1 Subscription', + 'search_subscriptions' => 'Search :count Subscriptions', + 'subdomain_is_not_available' => 'Subdomain is not available', + 'connect_gmail' => 'Connect Gmail', + 'disconnect_gmail' => 'Disconnect Gmail', + 'connected_gmail' => 'Successfully connected Gmail', + 'disconnected_gmail' => 'Successfully disconnected Gmail', + 'update_fail_help' => 'Changes to the codebase may be blocking the update, you can run this command to discard the changes:', + 'client_id_number' => 'Client ID Number', + 'count_minutes' => ':count Minutes', + 'password_timeout' => 'Password Timeout', + 'shared_invoice_credit_counter' => 'Shared Invoice/Credit Counter', 'activity_80' => ':user created subscription :subscription', 'activity_81' => ':user updated subscription :subscription', @@ -4212,6 +4228,13 @@ $LANG = array( 'activity_84' => ':user restored subscription :subscription', 'amount_greater_than_balance_v5' => 'The amount is greater than the invoice balance. You cannot overpay an invoice.', 'click_to_continue' => 'Click to continue', + + 'notification_invoice_created_subject' => 'Invoice :invoice was created to :client', + 'notification_invoice_created_subject' => 'Invoice :invoice was created for :client', + 'notification_quote_created_subject' => 'Quote :invoice was created to :client', + 'notification_quote_created_subject' => 'Quote :invoice was created for :client', + 'notification_credit_created_subject' => 'Credit :invoice was created to :client', + 'notification_credit_created_subject' => 'Credit :invoice was created for :client', ); return $LANG;